Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .sampo/changesets/cantankerous-duchess-loviatar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
hex/posthog: patch
---

Allow shared flag definition cache providers to support local evaluation without a secret key.
7 changes: 4 additions & 3 deletions lib/posthog/config_schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ defmodule PostHog.Config.Schema do
default: nil,
doc: """
A privileged project secret (`phs_`) or appropriately scoped personal API key (`phx_`)
used only to load definitions for local feature flag evaluation. Local evaluation is inert
and starts no poller when this value is absent or blank.
required only to fetch local feature flag definitions directly from PostHog. A configured
`flag_definition_cache_provider` can supply definitions without this key.
"""
],
enable_local_evaluation: [
type: :boolean,
default: true,
doc: "Enable local feature flag evaluation when a non-empty `secret_key` is configured."
doc:
"Enable local feature flag evaluation when a non-empty `secret_key` or a `flag_definition_cache_provider` is configured."
],
feature_flags_poll_interval_ms: [
type: :pos_integer,
Expand Down
3 changes: 2 additions & 1 deletion lib/posthog/feature_flags.ex
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ defmodule PostHog.FeatureFlags do
Evaluates feature flags for a `distinct_id` and returns a snapshot.

Returns `{:ok, %PostHog.FeatureFlags.Evaluations{}}` on success. Definitions
are evaluated locally first when privileged local evaluation is configured;
are evaluated locally first when local evaluation is configured with a
privileged key or a shared definition cache provider;
unresolved flags are filled by at most one `/flags` call. If that fallback
fails after some flags resolved locally, the successful local subset is
returned as a partial snapshot. If nothing resolved, the remote error is
Expand Down
16 changes: 16 additions & 0 deletions lib/posthog/feature_flags/definition_loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,14 @@ defmodule PostHog.FeatureFlags.DefinitionLoader do
defp refresh_state(%{config: %{flag_definition_cache_provider: nil}} = state),
do: fetch_direct(state, false)

# Cache-only readers must not acquire fetch leadership from the provider.
defp refresh_state(
%{
config: %{secret_key: nil, flag_definition_cache_provider: {module, provider_state}}
} = state
),
do: read_provider_without_fetch_ownership(state, module, provider_state)

defp refresh_state(
%{
config: %{flag_definition_cache_provider: {module, provider_state}}
Expand Down Expand Up @@ -410,6 +418,14 @@ defmodule PostHog.FeatureFlags.DefinitionLoader do
end
end

defp fetch_direct(%{config: %{secret_key: nil}} = state, _store?) do
Comment thread
dustinbyrne marked this conversation as resolved.
warning(
"definition refresh requires a secret_key to fetch from PostHog; keeping stale definitions"
)

state
end

defp fetch_direct(state, store?) do
config = state.config

Expand Down
13 changes: 11 additions & 2 deletions lib/posthog/feature_flags/flag_definition_cache_provider.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ defmodule PostHog.FeatureFlags.FlagDefinitionCacheProvider do
the definitions. Only version 2 selects explicit property matching; older
cached envelopes omitting the version use released service legacy matching.

With a project `:api_key` and local evaluation enabled, a provider can supply
definitions without a `:secret_key`. These cache-only readers read the provider
on each refresh without invoking the fetch decision or publishing definitions.
Instances with a secret key consult the fetch decision: true fetches and
publishes fresh definitions, while false reads the cache. An empty or failed
cache preserves loaded definitions; a keyed instance without definitions can
recover by fetching directly. Provider-backed instances use the same startup,
polling, and refresh lifecycle.

A minimal provider can coordinate fetching and keep the complete envelope in
an application-owned cache:

Expand All @@ -23,14 +32,14 @@ defmodule PostHog.FeatureFlags.FlagDefinitionCacheProvider do
end

config :posthog,
secret_key: System.fetch_env!("POSTHOG_SECRET_KEY"),
api_key: System.fetch_env!("POSTHOG_PROJECT_TOKEN"),
flag_definition_cache_provider: {MyApp.PostHogDefinitionCache, provider_state}
"""

@type provider_state :: any()
@type definitions :: map()

@doc "Returns whether this SDK instance should fetch fresh definitions from PostHog."
@doc "Returns whether a secret-key-backed SDK instance should fetch fresh definitions from PostHog."
@callback should_fetch_flag_definitions(provider_state()) :: boolean()

@doc "Reads definitions from the shared cache, or returns nil when it is empty."
Expand Down
5 changes: 3 additions & 2 deletions lib/posthog/supervisor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,12 @@ defmodule PostHog.Supervisor do
%{
enabled: true,
enable_local_evaluation: true,
secret_key: %PostHog.Config.Secret{value: secret_key}
secret_key: secret_key,
flag_definition_cache_provider: provider
} = config,
callers
)
when is_binary(secret_key) and secret_key != "" do
when not is_nil(secret_key) or not is_nil(provider) do
[
{PostHog.FeatureFlags.DefinitionLoader.NegativeKnowledge,
supervisor_name: config.supervisor_name},
Expand Down
Loading
Loading