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