Skip to content

Commit 9096a3a

Browse files
refactor: Move Posthog.feature_flag to Client.feature_flag
That's where it belongs
1 parent 55e180a commit 9096a3a

File tree

4 files changed

+390
-345
lines changed

4 files changed

+390
-345
lines changed

lib/posthog.ex

Lines changed: 5 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
require Logger
2-
31
defmodule Posthog do
42
@moduledoc """
53
A comprehensive Elixir client for PostHog's analytics and feature flag APIs.
@@ -142,24 +140,10 @@ defmodule Posthog do
142140
# Event with custom headers
143141
Posthog.capture("login", "user_123", %{}, headers: [{"x-forwarded-for", "127.0.0.1"}])
144142
"""
145-
@typep result() :: {:ok, term()} | {:error, term()}
146-
@typep cache_key() :: {:feature_flag_called, binary(), binary()}
147-
@typep feature_flag_called_event_properties_key() ::
148-
:"$feature_flag"
149-
| :"$feature_flag_response"
150-
| :"$feature_flag_id"
151-
| :"$feature_flag_version"
152-
| :"$feature_flag_reason"
153-
| :"$feature_flag_request_id"
154-
| :distinct_id
155-
@typep feature_flag_called_event_properties() :: %{
156-
feature_flag_called_event_properties_key() => any() | nil
157-
}
158-
159143
alias Posthog.{Client, FeatureFlag}
160144

161145
@spec capture(Client.event(), Client.distinct_id(), Client.properties(), Client.opts()) ::
162-
result()
146+
Client.result()
163147
defdelegate capture(event, distinct_id, properties, opts \\ []), to: Client
164148

165149
@doc """
@@ -179,7 +163,7 @@ defmodule Posthog do
179163
180164
Posthog.batch(events)
181165
"""
182-
@spec batch(list(tuple()), keyword()) :: result()
166+
@spec batch(list(tuple()), keyword()) :: Client.result()
183167
defdelegate batch(events, opts \\ []), to: Client
184168

185169
@doc """
@@ -207,7 +191,7 @@ defmodule Posthog do
207191
group_properties: %{company: %{industry: "tech"}}
208192
)
209193
"""
210-
@spec feature_flags(binary(), keyword()) :: result()
194+
@spec feature_flags(binary(), keyword()) :: Client.result()
211195
defdelegate feature_flags(distinct_id, opts \\ []), to: Client
212196

213197
@doc """
@@ -233,106 +217,8 @@ defmodule Posthog do
233217
# enabled: "variant-a"
234218
# }
235219
"""
236-
@spec feature_flag(binary(), binary(), Client.feature_flag_opts()) :: result()
237-
def feature_flag(flag, distinct_id, opts \\ []) do
238-
with {:ok, response} <- Client._decide_request(distinct_id, opts),
239-
enabled when not is_nil(enabled) <- response.feature_flags[flag] do
240-
# Only capture if send_feature_flag_event is true (default)
241-
if Keyword.get(opts, :send_feature_flag_event, true),
242-
do:
243-
capture_feature_flag_called_event(
244-
distinct_id,
245-
%{
246-
"$feature_flag" => flag,
247-
"$feature_flag_response" => enabled
248-
},
249-
response
250-
)
251-
252-
{:ok, FeatureFlag.new(flag, enabled, Map.get(response.feature_flag_payloads, flag))}
253-
else
254-
{:error, _} = err -> err
255-
nil -> {:error, :not_found}
256-
end
257-
end
258-
259-
@spec capture_feature_flag_called_event(
260-
Client.distinct_id(),
261-
feature_flag_called_event_properties(),
262-
map()
263-
) ::
264-
:ok
265-
defp capture_feature_flag_called_event(distinct_id, properties, response) do
266-
# Create a unique key for this distinct_id and flag combination
267-
cache_key = {:feature_flag_called, distinct_id, properties["$feature_flag"]}
268-
269-
# Check if we've seen this combination before using Cachex
270-
case Cachex.exists?(Posthog.Application.cache_name(), cache_key) do
271-
{:ok, false} ->
272-
do_capture_feature_flag_called_event(cache_key, distinct_id, properties, response)
273-
274-
# Should be `{:error, :no_cache}` but Dyalixir is wrongly assuming that doesn't exist
275-
{:error, _} ->
276-
# Cache doesn't exist, let's capture the event PLUS notify user they should be initing it
277-
do_capture_feature_flag_called_event(cache_key, distinct_id, properties, response)
278-
279-
Logger.error("""
280-
[posthog] Cachex process `#{inspect(Posthog.Application.cache_name())}` is not running.
281-
282-
➤ This likely means you forgot to include `posthog` as an application dependency (mix.exs):
283-
284-
Example:
285-
286-
extra_applications: [..., :posthog]
287-
288-
289-
➤ Or, add `Posthog.Application` to your supervision tree (lib/my_lib/application.ex).
290-
291-
Example:
292-
{Posthog.Application, []}
293-
""")
294-
295-
{:ok, true} ->
296-
# Entry already exists, no need to do anything
297-
:ok
298-
end
299-
end
300-
301-
@spec do_capture_feature_flag_called_event(
302-
cache_key(),
303-
Client.distinct_id(),
304-
feature_flag_called_event_properties(),
305-
map()
306-
) :: :ok
307-
defp do_capture_feature_flag_called_event(cache_key, distinct_id, properties, response) do
308-
flag = properties["$feature_flag"]
309-
310-
properties =
311-
if Map.has_key?(response, :flags) do
312-
Map.merge(properties, %{
313-
"$feature_flag_id" => response.flags[flag]["metadata"]["id"],
314-
"$feature_flag_version" => response.flags[flag]["metadata"]["version"],
315-
"$feature_flag_reason" => response.flags[flag]["reason"]["description"]
316-
})
317-
else
318-
properties
319-
end
320-
321-
properties =
322-
if Map.get(response, :request_id) do
323-
Map.put(properties, "$feature_flag_request_id", response.request_id)
324-
else
325-
properties
326-
end
327-
328-
# Send the event to our server
329-
Client.capture("$feature_flag_called", distinct_id, properties, [])
330-
331-
# Add new entry to cache using Cachex
332-
Cachex.put(Posthog.Application.cache_name(), cache_key, true)
333-
334-
:ok
335-
end
220+
@spec feature_flag(binary(), binary(), Client.feature_flag_opts()) :: Client.result()
221+
defdelegate feature_flag(flag, distinct_id, opts \\ []), to: Client
336222

337223
@doc """
338224
Checks if a feature flag is enabled for a given distinct ID.

lib/posthog/client.ex

Lines changed: 155 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require Logger
2+
13
defmodule Posthog.Client do
24
@moduledoc """
35
Low-level HTTP client for interacting with PostHog's API.
@@ -72,6 +74,13 @@ defmodule Posthog.Client do
7274
Posthog.Client.feature_flags("user_123", groups: %{team: "engineering"})
7375
"""
7476

77+
alias Posthog.FeatureFlag
78+
79+
@typedoc """
80+
Result of a PostHog operation.
81+
"""
82+
@type result() :: {:ok, response()} | {:error, response() | term()}
83+
7584
@typedoc """
7685
HTTP headers in the format expected by :hackney.
7786
"""
@@ -129,6 +138,23 @@ defmodule Posthog.Client do
129138
"""
130139
@type feature_flag_opts :: opts() | [send_feature_flag_event: boolean()]
131140

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+
132158
# Adds default headers to the request.
133159
#
134160
# ## Parameters
@@ -166,8 +192,7 @@ defmodule Posthog.Client do
166192
# Event with custom headers
167193
Posthog.Client.capture("login", "user_123", %{}, headers: [{"x-forwarded-for", "127.0.0.1"}])
168194
"""
169-
@spec capture(event(), distinct_id(), properties(), opts()) ::
170-
{:ok, response()} | {:error, response() | term()}
195+
@spec capture(event(), distinct_id(), properties(), opts()) :: result()
171196
def capture(event, distinct_id, properties \\ %{}, opts \\ []) when is_list(opts) do
172197
if Posthog.Config.enabled_capture?() do
173198
posthog_event = Posthog.Event.new(event, distinct_id, properties, opts)
@@ -195,8 +220,7 @@ defmodule Posthog.Client do
195220
196221
Posthog.Client.batch(events, %{timestamp: DateTime.utc_now()})
197222
"""
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()
200224
def batch(events, opts) when is_list(opts) do
201225
batch(events, opts, headers(opts[:headers]))
202226
end
@@ -235,8 +259,7 @@ defmodule Posthog.Client do
235259
group_properties: %{company: %{industry: "tech"}}
236260
)
237261
"""
238-
@spec feature_flags(binary(), opts()) ::
239-
{:ok, Posthog.FeatureFlag.flag_response()} | {:error, response() | term()}
262+
@spec feature_flags(binary(), opts()) :: result()
240263
def feature_flags(distinct_id, opts) do
241264
case _decide_request(distinct_id, opts) do
242265
{:ok, response} ->
@@ -251,6 +274,132 @@ defmodule Posthog.Client do
251274
end
252275
end
253276

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+
254403
@doc false
255404
def _decide_request(distinct_id, opts) do
256405
body =

0 commit comments

Comments
 (0)