1
+ require Logger
2
+
1
3
defmodule Posthog.Client do
2
4
@ moduledoc """
3
5
Low-level HTTP client for interacting with PostHog's API.
@@ -72,6 +74,13 @@ defmodule Posthog.Client do
72
74
Posthog.Client.feature_flags("user_123", groups: %{team: "engineering"})
73
75
"""
74
76
77
+ alias Posthog.FeatureFlag
78
+
79
+ @ typedoc """
80
+ Result of a PostHog operation.
81
+ """
82
+ @ type result ( ) :: { :ok , response ( ) } | { :error , response ( ) | term ( ) }
83
+
75
84
@ typedoc """
76
85
HTTP headers in the format expected by :hackney.
77
86
"""
@@ -129,6 +138,23 @@ defmodule Posthog.Client do
129
138
"""
130
139
@ type feature_flag_opts :: opts ( ) | [ send_feature_flag_event: boolean ( ) ]
131
140
141
+ @ typedoc """
142
+ Cache key for the `$feature_flag_called` event.
143
+ """
144
+ @ type cache_key ( ) :: { :feature_flag_called , binary ( ) , binary ( ) }
145
+
146
+ @ typep feature_flag_called_event_properties_key ( ) ::
147
+ :"$feature_flag"
148
+ | :"$feature_flag_response"
149
+ | :"$feature_flag_id"
150
+ | :"$feature_flag_version"
151
+ | :"$feature_flag_reason"
152
+ | :"$feature_flag_request_id"
153
+ | :distinct_id
154
+ @ typep feature_flag_called_event_properties ( ) :: % {
155
+ feature_flag_called_event_properties_key ( ) => any ( ) | nil
156
+ }
157
+
132
158
# Adds default headers to the request.
133
159
#
134
160
# ## Parameters
@@ -166,8 +192,7 @@ defmodule Posthog.Client do
166
192
# Event with custom headers
167
193
Posthog.Client.capture("login", "user_123", %{}, headers: [{"x-forwarded-for", "127.0.0.1"}])
168
194
"""
169
- @ spec capture ( event ( ) , distinct_id ( ) , properties ( ) , opts ( ) ) ::
170
- { :ok , response ( ) } | { :error , response ( ) | term ( ) }
195
+ @ spec capture ( event ( ) , distinct_id ( ) , properties ( ) , opts ( ) ) :: result ( )
171
196
def capture ( event , distinct_id , properties \\ % { } , opts \\ [ ] ) when is_list ( opts ) do
172
197
if Posthog.Config . enabled_capture? ( ) do
173
198
posthog_event = Posthog.Event . new ( event , distinct_id , properties , opts )
@@ -195,8 +220,7 @@ defmodule Posthog.Client do
195
220
196
221
Posthog.Client.batch(events, %{timestamp: DateTime.utc_now()})
197
222
"""
198
- @ spec batch ( [ { event ( ) , distinct_id ( ) , properties ( ) } ] , opts ( ) , headers ( ) ) ::
199
- { :ok , response ( ) } | { :error , response ( ) | term ( ) }
223
+ @ spec batch ( [ { event ( ) , distinct_id ( ) , properties ( ) } ] , opts ( ) , headers ( ) ) :: result ( )
200
224
def batch ( events , opts ) when is_list ( opts ) do
201
225
batch ( events , opts , headers ( opts [ :headers ] ) )
202
226
end
@@ -235,8 +259,7 @@ defmodule Posthog.Client do
235
259
group_properties: %{company: %{industry: "tech"}}
236
260
)
237
261
"""
238
- @ spec feature_flags ( binary ( ) , opts ( ) ) ::
239
- { :ok , Posthog.FeatureFlag . flag_response ( ) } | { :error , response ( ) | term ( ) }
262
+ @ spec feature_flags ( binary ( ) , opts ( ) ) :: result ( )
240
263
def feature_flags ( distinct_id , opts ) do
241
264
case _decide_request ( distinct_id , opts ) do
242
265
{ :ok , response } ->
@@ -251,6 +274,132 @@ defmodule Posthog.Client do
251
274
end
252
275
end
253
276
277
+ @ doc """
278
+ Retrieves information about a specific feature flag for a given distinct ID.
279
+
280
+ ## Parameters
281
+
282
+ * `flag` - The name of the feature flag
283
+ * `distinct_id` - The unique identifier for the user
284
+ * `opts` - Optional parameters for the feature flag request
285
+
286
+ ## Examples
287
+
288
+ # Boolean feature flag
289
+ {:ok, flag} = Posthog.feature_flag("new-dashboard", "user_123")
290
+ # Returns: %Posthog.FeatureFlag{name: "new-dashboard", payload: true, enabled: true}
291
+
292
+ # Multivariate feature flag
293
+ {:ok, flag} = Posthog.feature_flag("pricing-test", "user_123")
294
+ # Returns: %Posthog.FeatureFlag{
295
+ # name: "pricing-test",
296
+ # payload: %{"price" => 99, "period" => "monthly"},
297
+ # enabled: "variant-a"
298
+ # }
299
+ """
300
+ @ spec feature_flag ( binary ( ) , binary ( ) , feature_flag_opts ( ) ) :: result ( )
301
+ def feature_flag ( flag , distinct_id , opts \\ [ ] ) do
302
+ with { :ok , response } <- _decide_request ( distinct_id , opts ) ,
303
+ enabled when not is_nil ( enabled ) <- response . feature_flags [ flag ] do
304
+ # Only capture if send_feature_flag_event is true (default)
305
+ if Keyword . get ( opts , :send_feature_flag_event , true ) ,
306
+ do:
307
+ capture_feature_flag_called_event (
308
+ distinct_id ,
309
+ % {
310
+ "$feature_flag" => flag ,
311
+ "$feature_flag_response" => enabled
312
+ } ,
313
+ response
314
+ )
315
+
316
+ { :ok , FeatureFlag . new ( flag , enabled , Map . get ( response . feature_flag_payloads , flag ) ) }
317
+ else
318
+ { :error , _ } = err -> err
319
+ nil -> { :error , :not_found }
320
+ end
321
+ end
322
+
323
+ @ spec capture_feature_flag_called_event (
324
+ distinct_id ( ) ,
325
+ feature_flag_called_event_properties ( ) ,
326
+ map ( )
327
+ ) ::
328
+ :ok
329
+ defp capture_feature_flag_called_event ( distinct_id , properties , response ) do
330
+ # Create a unique key for this distinct_id and flag combination
331
+ cache_key = { :feature_flag_called , distinct_id , properties [ "$feature_flag" ] }
332
+
333
+ # Check if we've seen this combination before using Cachex
334
+ case Cachex . exists? ( Posthog.Application . cache_name ( ) , cache_key ) do
335
+ { :ok , false } ->
336
+ do_capture_feature_flag_called_event ( cache_key , distinct_id , properties , response )
337
+
338
+ # Should be `{:error, :no_cache}` but Dyalixir is wrongly assuming that doesn't exist
339
+ { :error , _ } ->
340
+ # Cache doesn't exist, let's capture the event PLUS notify user they should be initing it
341
+ do_capture_feature_flag_called_event ( cache_key , distinct_id , properties , response )
342
+
343
+ Logger . error ( """
344
+ [posthog] Cachex process `#{ inspect ( Posthog.Application . cache_name ( ) ) } ` is not running.
345
+
346
+ ➤ This likely means you forgot to include `posthog` as an application dependency (mix.exs):
347
+
348
+ Example:
349
+
350
+ extra_applications: [..., :posthog]
351
+
352
+
353
+ ➤ Or, add `Posthog.Application` to your supervision tree (lib/my_lib/application.ex).
354
+
355
+ Example:
356
+ {Posthog.Application, []}
357
+ """ )
358
+
359
+ { :ok , true } ->
360
+ # Entry already exists, no need to do anything
361
+ :ok
362
+ end
363
+ end
364
+
365
+ @ spec do_capture_feature_flag_called_event (
366
+ cache_key ( ) ,
367
+ distinct_id ( ) ,
368
+ feature_flag_called_event_properties ( ) ,
369
+ map ( )
370
+ ) :: :ok
371
+ defp do_capture_feature_flag_called_event ( cache_key , distinct_id , properties , response ) do
372
+ flag = properties [ "$feature_flag" ]
373
+
374
+ properties =
375
+ if Map . has_key? ( response , :flags ) do
376
+ Map . merge ( properties , % {
377
+ "$feature_flag_id" => response . flags [ flag ] [ "metadata" ] [ "id" ] ,
378
+ "$feature_flag_version" => response . flags [ flag ] [ "metadata" ] [ "version" ] ,
379
+ "$feature_flag_reason" => response . flags [ flag ] [ "reason" ] [ "description" ]
380
+ } )
381
+ else
382
+ properties
383
+ end
384
+
385
+ properties =
386
+ if Map . get ( response , :request_id ) do
387
+ Map . put ( properties , "$feature_flag_request_id" , response . request_id )
388
+ else
389
+ properties
390
+ end
391
+
392
+ # Send the event to our server
393
+ # NOTE: Calling this with `Posthog.Client.capture/4` rather than `capture/4`
394
+ # because mocks won't work properly unless we use the fully defined function
395
+ Posthog.Client . capture ( "$feature_flag_called" , distinct_id , properties , [ ] )
396
+
397
+ # Add new entry to cache using Cachex
398
+ Cachex . put ( Posthog.Application . cache_name ( ) , cache_key , true )
399
+
400
+ :ok
401
+ end
402
+
254
403
@ doc false
255
404
def _decide_request ( distinct_id , opts ) do
256
405
body =
0 commit comments