@@ -3,6 +3,8 @@ defmodule PostHog.FeatureFlags do
33 Convenience functions to work with Feature Flags API
44 """
55
6+ alias PostHog.FeatureFlagResult
7+
68 @ doc """
79 Make request to [`/flags`](https://posthog.com/docs/api/flags) API.
810
@@ -125,37 +127,202 @@ defmodule PostHog.FeatureFlags do
125127 @ spec check ( PostHog . supervisor_name ( ) , String . t ( ) , PostHog . distinct_id ( ) | map ( ) | nil ) ::
126128 { :ok , boolean ( ) } | { :ok , String . t ( ) } | { :error , Exception . t ( ) }
127129 def check ( name \\ PostHog , flag_name , distinct_id_or_body \\ nil ) do
128- with { :ok , % { distinct_id: distinct_id } = body } <- body_for_flags ( distinct_id_or_body ) ,
129- { :ok , % { body: body } } <- flags ( name , body ) do
130- result =
131- case body do
132- % { "flags" => % { ^ flag_name => % { "variant" => variant } } } when not is_nil ( variant ) ->
133- { :ok , variant }
130+ case evaluate_flag ( name , flag_name , distinct_id_or_body , [ ] ) do
131+ { :ok , % FeatureFlagResult { } = flag_result , _resp_body } ->
132+ { :ok , FeatureFlagResult . value ( flag_result ) }
134133
135- % { "flags" => % { ^ flag_name => % { "enabled" => true } } } ->
136- { :ok , true }
134+ { :ok , nil , resp_body } ->
135+ { :error ,
136+ % PostHog.UnexpectedResponseError {
137+ response: resp_body ,
138+ message: "Feature flag #{ flag_name } was not found in the response"
139+ } }
137140
138- % { "flags" => % { ^ flag_name => _ } } ->
139- { :ok , false }
141+ { :error , reason , _resp_body } ->
142+ { :error , reason }
143+ end
144+ end
140145
141- % { "flags" => _ } ->
142- { :error ,
143- % PostHog.UnexpectedResponseError {
144- response: body ,
145- message: "Feature flag #{ flag_name } was not found in the response"
146- } }
147- end
146+ @ doc false
147+ def get_feature_flag_result ( flag_name , distinct_id_or_body )
148+ when not is_atom ( flag_name ) and not is_list ( distinct_id_or_body ) ,
149+ do: get_feature_flag_result ( PostHog , flag_name , distinct_id_or_body , [ ] )
150+
151+ @ doc false
152+ def get_feature_flag_result ( flag_name , distinct_id_or_body , opts )
153+ when not is_atom ( flag_name ) and is_list ( opts ) ,
154+ do: get_feature_flag_result ( PostHog , flag_name , distinct_id_or_body , opts )
155+
156+ @ doc """
157+ Gets the full feature flag result including value and payload.
158+
159+ Returns `{:ok, %FeatureFlagResult{}}` on success, `{:ok, nil}` if the flag
160+ is not found, or `{:error, reason}` on failure.
161+
162+ The `FeatureFlagResult` struct contains:
163+ - `key` - The flag name
164+ - `enabled` - Whether the flag is enabled
165+ - `variant` - The variant string (nil for boolean flags)
166+ - `payload` - The JSON payload configured for the flag (nil if not set)
167+
168+ By default, this function will
169+ [send](https://posthog.com/docs/api/flags#step-3-send-a-feature_flag_called-event)
170+ a `$feature_flag_called` event and
171+ [set](https://posthog.com/docs/api/flags#step-2-include-feature-flag-information-when-capturing-events)
172+ the `$feature/feature-flag-name` property in context.
173+
174+ ## Options
175+
176+ - `:send_event` - Whether to send the `$feature_flag_called` event. Defaults to `true`.
177+
178+ ## Examples
179+
180+ Get feature flag result for `distinct_id`:
181+
182+ iex> PostHog.FeatureFlags.get_feature_flag_result("example-feature-flag-1", "user123")
183+ {:ok, %PostHog.FeatureFlagResult{key: "example-feature-flag-1", enabled: true, variant: nil, payload: nil}}
184+
185+ Get feature flag result with payload:
186+
187+ iex> PostHog.FeatureFlags.get_feature_flag_result("feature-with-payload", "user123")
188+ {:ok, %PostHog.FeatureFlagResult{key: "feature-with-payload", enabled: true, variant: "variant1", payload: %{"key" => "value"}}}
189+
190+ Get feature flag result without sending event:
191+
192+ iex> PostHog.FeatureFlags.get_feature_flag_result("my-flag", "user123", send_event: false)
193+ {:ok, %PostHog.FeatureFlagResult{key: "my-flag", enabled: true, variant: nil, payload: nil}}
194+
195+ Flag not found returns `{:ok, nil}`:
196+
197+ iex> PostHog.FeatureFlags.get_feature_flag_result("non-existent-flag", "user123")
198+ {:ok, nil}
199+
200+ Get feature flag result for `distinct_id` in the current context:
148201
149- evaluated_at = Map . get ( body , "evaluatedAt" )
202+ iex> PostHog.set_context(%{distinct_id: "user123"})
203+ iex> PostHog.FeatureFlags.get_feature_flag_result("example-feature-flag-1")
204+ {:ok, %PostHog.FeatureFlagResult{key: "example-feature-flag-1", enabled: true, variant: nil, payload: nil}}
150205
151- # Make sure we keep track of the feature flag usage for debugging purposes
152- # Users are NOT charged extra for this, but it's still good to have.
153- log_feature_flag_usage ( name , distinct_id , flag_name , result , evaluated_at )
206+ Get feature flag result through a named PostHog instance:
154207
155- result
208+ PostHog.FeatureFlags.get_feature_flag_result(MyPostHog, "example-feature-flag-1", "user123")
209+ """
210+ @ spec get_feature_flag_result (
211+ PostHog . supervisor_name ( ) ,
212+ String . t ( ) ,
213+ PostHog . distinct_id ( ) | map ( ) | nil ,
214+ keyword ( )
215+ ) ::
216+ { :ok , FeatureFlagResult . t ( ) | nil } | { :error , Exception . t ( ) }
217+ def get_feature_flag_result ( name \\ PostHog , flag_name , distinct_id_or_body \\ nil , opts \\ [ ] ) do
218+ case evaluate_flag ( name , flag_name , distinct_id_or_body , opts ) do
219+ { :ok , % FeatureFlagResult { } = result , _resp_body } -> { :ok , result }
220+ { :ok , nil , _resp_body } -> { :ok , nil }
221+ { :error , reason , _resp_body } -> { :error , reason }
156222 end
157223 end
158224
225+ @ doc false
226+ def get_feature_flag_result! ( flag_name , distinct_id_or_body )
227+ when not is_atom ( flag_name ) and not is_list ( distinct_id_or_body ) ,
228+ do: get_feature_flag_result! ( PostHog , flag_name , distinct_id_or_body , [ ] )
229+
230+ @ doc false
231+ def get_feature_flag_result! ( flag_name , distinct_id_or_body , opts )
232+ when not is_atom ( flag_name ) and is_list ( opts ) ,
233+ do: get_feature_flag_result! ( PostHog , flag_name , distinct_id_or_body , opts )
234+
235+ @ doc """
236+ Gets the full feature flag result or raises on error.
237+
238+ This is a wrapper around `get_feature_flag_result/4` that returns the result
239+ directly or raises an exception on error. This follows the Elixir convention
240+ where functions ending with `!` raise exceptions instead of returning error
241+ tuples.
242+
243+ Returns `nil` if the flag is not found (does not raise), consistent with
244+ other PostHog SDKs.
245+
246+ > **Warning**: Use this function with care as it will raise an error if there
247+ > are any API errors (e.g. missing `distinct_id`). For more resilient code,
248+ > use `get_feature_flag_result/4` which returns `{:error, reason}` instead of
249+ > raising.
250+
251+ ## Options
252+
253+ - `:send_event` - Whether to send the `$feature_flag_called` event. Defaults to `true`.
254+
255+ ## Examples
256+
257+ Get feature flag result for `distinct_id`:
258+
259+ iex> PostHog.FeatureFlags.get_feature_flag_result!("example-feature-flag-1", "user123")
260+ %PostHog.FeatureFlagResult{key: "example-feature-flag-1", enabled: true, variant: nil, payload: nil}
261+
262+ Returns `nil` when flag is not found:
263+
264+ iex> PostHog.FeatureFlags.get_feature_flag_result!("non-existent-flag", "user123")
265+ nil
266+
267+ Raises an error when `distinct_id` is missing:
268+
269+ iex> PostHog.FeatureFlags.get_feature_flag_result!("example-feature-flag-1")
270+ ** (PostHog.Error) distinct_id is required but wasn't explicitly provided or found in the context
271+ """
272+ @ spec get_feature_flag_result! (
273+ PostHog . supervisor_name ( ) ,
274+ String . t ( ) ,
275+ PostHog . distinct_id ( ) | map ( ) | nil ,
276+ keyword ( )
277+ ) ::
278+ FeatureFlagResult . t ( ) | nil | no_return ( )
279+ def get_feature_flag_result! ( name \\ PostHog , flag_name , distinct_id_or_body \\ nil , opts \\ [ ] ) do
280+ case get_feature_flag_result ( name , flag_name , distinct_id_or_body , opts ) do
281+ { :ok , result } -> result
282+ { :error , error } -> raise error
283+ end
284+ end
285+
286+ defp evaluate_flag ( name , flag_name , distinct_id_or_body , opts ) do
287+ send_event = Keyword . get ( opts , :send_event , true )
288+
289+ with { :ok , % { distinct_id: distinct_id } = body } <- body_for_flags ( distinct_id_or_body ) ,
290+ { :ok , % { body: resp_body } } <- flags ( name , body ) do
291+ case resp_body do
292+ % { "flags" => % { ^ flag_name => flag_data } } ->
293+ { enabled , variant } = extract_flag_enabled_and_variant ( flag_data )
294+ payload = get_in ( flag_data , [ "metadata" , "payload" ] )
295+
296+ flag_result = % FeatureFlagResult {
297+ key: flag_name ,
298+ enabled: enabled ,
299+ variant: variant ,
300+ payload: payload
301+ }
302+
303+ evaluated_at = Map . get ( resp_body , "evaluatedAt" )
304+
305+ if send_event do
306+ value = FeatureFlagResult . value ( flag_result )
307+ log_feature_flag_usage ( name , distinct_id , flag_name , { :ok , value } , evaluated_at )
308+ end
309+
310+ { :ok , flag_result , resp_body }
311+
312+ % { "flags" => _ } ->
313+ { :ok , nil , resp_body }
314+ end
315+ else
316+ { :error , reason } -> { :error , reason , nil }
317+ end
318+ end
319+
320+ defp extract_flag_enabled_and_variant ( flag_data ) do
321+ enabled = Map . get ( flag_data , "enabled" , false ) == true
322+ variant = Map . get ( flag_data , "variant" )
323+ { enabled , variant }
324+ end
325+
159326 @ doc false
160327 def check! ( flag_name , distinct_id_or_body ) when not is_atom ( flag_name ) ,
161328 do: check! ( PostHog , flag_name , distinct_id_or_body )
0 commit comments