Skip to content

Commit 191d1e6

Browse files
committed
feat(flags): add local feature flag evaluation
1 parent b02fce7 commit 191d1e6

20 files changed

Lines changed: 4002 additions & 50 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
hex/posthog: minor
3+
---
4+
5+
Add local feature flag definition polling and evaluation with safe remote fallback.

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# posthog
22

3+
## Unreleased
4+
5+
### Minor changes
6+
7+
- Add privileged, polled local feature flag evaluation with safe remote fallback, ETag definition loading, person/group/cohort/dependency matching, deterministic variants and payloads, and an optional bounded shared definition cache provider. Configure it with `secret_key`; existing installations without a secret remain remote-only.
8+
39
## 2.14.2 — 2026-08-25
410

511
### Patch changes

lib/posthog/api.ex

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,17 @@ defmodule PostHog.API do
1313
max_retries: client.feature_flags_request_max_retries
1414
)
1515
end
16+
17+
def flag_definitions(%__MODULE__.Client{} = client, project_api_key, secret_key, etag) do
18+
headers =
19+
[{"authorization", "Bearer #{secret_key}"}]
20+
|> then(fn headers ->
21+
if is_binary(etag), do: [{"if-none-match", etag} | headers], else: headers
22+
end)
23+
24+
client.module.request(client.client, :get, "/flags/definitions",
25+
params: %{token: project_api_key, send_cohorts: true},
26+
headers: headers
27+
)
28+
end
1629
end

lib/posthog/api/client.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ defmodule PostHog.API.Client do
121121
Response tuple returned by `c:request/4`.
122122
123123
Successful responses must expose at least a numeric `:status` and decoded
124-
`:body`; errors should return the exception or error struct from the HTTP
125-
client.
124+
`:body`; they may also expose response `:headers`. Errors should return the
125+
exception or error struct from the HTTP client.
126126
"""
127127
@type response() :: {:ok, %{status: non_neg_integer(), body: any()}} | {:error, Exception.t()}
128128

lib/posthog/config.ex

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,45 @@ defmodule PostHog.Config do
3838
doc:
3939
"Number of retries for /flags requests after network, transport, or timeout failures. Set to 0 to disable retries."
4040
],
41+
secret_key: [
42+
type: {:or, [:string, nil]},
43+
default: nil,
44+
doc: """
45+
A privileged project secret (`phs_`) or appropriately scoped personal API key (`phx_`)
46+
used only to load definitions for local feature flag evaluation. Local evaluation is inert
47+
and starts no poller when this value is absent or blank.
48+
"""
49+
],
50+
enable_local_evaluation: [
51+
type: :boolean,
52+
default: true,
53+
doc:
54+
"Enable local feature flag evaluation when a non-empty `secret_key` is configured."
55+
],
56+
feature_flags_poll_interval_ms: [
57+
type: :pos_integer,
58+
default: 30_000,
59+
doc:
60+
"Interval in milliseconds between local feature flag definition refreshes."
61+
],
62+
flag_definition_cache_provider: [
63+
type: {:or, [{:tuple, [:atom, :any]}, nil]},
64+
default: nil,
65+
doc:
66+
"Optional `{module, state}` implementing `PostHog.FeatureFlags.FlagDefinitionCacheProvider`."
67+
],
68+
flag_definition_cache_provider_timeout_ms: [
69+
type: :pos_integer,
70+
default: 5_000,
71+
doc:
72+
"Maximum time in milliseconds allowed for each definition cache provider callback."
73+
],
74+
flag_definition_request_timeout_ms: [
75+
type: :pos_integer,
76+
default: 10_000,
77+
doc:
78+
"Maximum time in milliseconds allowed for a local feature flag definition request."
79+
],
4180
supervisor_name: [
4281
type: :atom,
4382
default: PostHog,
@@ -219,7 +258,7 @@ defmodule PostHog.Config do
219258
220259
## Remarks
221260
222-
String `:api_key` and `:api_host` values are trimmed before validation. A blank
261+
String `:api_key`, `:secret_key`, and `:api_host` values are trimmed before validation. A blank
223262
`:api_host` falls back to the default PostHog US ingestion host.
224263
"""
225264
@spec validate(options()) ::
@@ -274,6 +313,13 @@ defmodule PostHog.Config do
274313
normalized_options
275314
end
276315
end)
316+
|> then(fn normalized_options ->
317+
if Keyword.has_key?(normalized_options, :secret_key) do
318+
Keyword.update!(normalized_options, :secret_key, &normalize_secret_key/1)
319+
else
320+
normalized_options
321+
end
322+
end)
277323
|> then(fn normalized_options ->
278324
if Keyword.has_key?(normalized_options, :api_host) do
279325
Keyword.update!(normalized_options, :api_host, &normalize_api_host/1)
@@ -287,6 +333,15 @@ defmodule PostHog.Config do
287333
defp normalize_api_key(nil), do: ""
288334
defp normalize_api_key(api_key), do: api_key
289335

336+
defp normalize_secret_key(secret_key) when is_binary(secret_key) do
337+
case String.trim(secret_key) do
338+
"" -> nil
339+
value -> value
340+
end
341+
end
342+
343+
defp normalize_secret_key(secret_key), do: secret_key
344+
290345
defp normalize_api_host(api_host) when is_binary(api_host) do
291346
api_host
292347
|> String.trim()

0 commit comments

Comments
 (0)