Skip to content

Commit 9b8c895

Browse files
feat: Extract Posthog.Event to its own class
Code looks much leaner and more extensible this way
1 parent 1ff1469 commit 9b8c895

File tree

4 files changed

+300
-176
lines changed

4 files changed

+300
-176
lines changed

lib/posthog/client.ex

Lines changed: 7 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,6 @@ defmodule Posthog.Client do
129129
"""
130130
@type feature_flag_opts :: opts() | [send_feature_flag_event: boolean()]
131131

132-
@lib_version Mix.Project.config()[:version]
133-
@lib_name "posthog-elixir"
134-
135-
import Posthog.Guard, only: [is_keyword_list: 1]
136-
137132
# Adds default headers to the request.
138133
#
139134
# ## Parameters
@@ -175,18 +170,8 @@ defmodule Posthog.Client do
175170
{:ok, response()} | {:error, response() | term()}
176171
def capture(event, distinct_id, properties \\ %{}, opts \\ []) when is_list(opts) do
177172
if Posthog.Config.enabled_capture?() do
178-
timestamp =
179-
Keyword.get_lazy(opts, :timestamp, fn ->
180-
DateTime.utc_now() |> DateTime.to_iso8601()
181-
end)
182-
183-
uuid = Keyword.get(opts, :uuid)
184-
185-
post!(
186-
"/capture",
187-
build_event(event, distinct_id, properties, timestamp, uuid),
188-
headers(opts[:headers])
189-
)
173+
posthog_event = Posthog.Event.new(event, distinct_id, properties, opts)
174+
post!("/capture", Posthog.Event.to_api_payload(posthog_event), headers(opts[:headers]))
190175
else
191176
disabled_capture_response()
192177
end
@@ -220,11 +205,12 @@ defmodule Posthog.Client do
220205
if Posthog.Config.enabled_capture?() do
221206
timestamp = Keyword.get_lazy(opts, :timestamp, fn -> DateTime.utc_now() end)
222207

223-
body =
224-
for {event, distinct_id, properties} <- events,
225-
do: build_event(event, distinct_id, properties, timestamp)
208+
posthog_events =
209+
for {event, distinct_id, properties} <- events do
210+
Posthog.Event.new(event, distinct_id, properties, timestamp: timestamp)
211+
end
226212

227-
post!("/capture", %{batch: body}, headers)
213+
post!("/capture", Posthog.Event.batch_payload(posthog_events), headers)
228214
else
229215
disabled_capture_response()
230216
end
@@ -278,54 +264,6 @@ defmodule Posthog.Client do
278264
end
279265
end
280266

281-
@doc """
282-
Builds an event payload for the PostHog API.
283-
284-
## Parameters
285-
286-
* `event` - The name of the event
287-
* `distinct_id` - The distinct ID for the person or group
288-
* `properties` - Event properties
289-
* `timestamp` - Optional timestamp for the event
290-
* `uuid` - Optional UUID for the event
291-
292-
## Examples
293-
294-
build_event("page_view", "user_123", nil)
295-
build_event("purchase", "user_123", %{price: 99.99}, DateTime.utc_now())
296-
build_event("purchase", "user_123", %{price: 99.99}, DateTime.utc_now(), Uniq.UUID.uuid7())
297-
"""
298-
@spec build_event(event(), distinct_id(), properties(), timestamp(), Uniq.UUID.t() | nil) ::
299-
map()
300-
def build_event(event, distinct_id, properties, timestamp, uuid \\ nil) do
301-
properties = Map.merge(lib_properties(), deep_stringify_keys(Map.new(properties)))
302-
uuid = uuid || Uniq.UUID.uuid7()
303-
304-
%{
305-
event: to_string(event),
306-
distinct_id: distinct_id,
307-
properties: properties,
308-
uuid: uuid,
309-
timestamp: timestamp
310-
}
311-
end
312-
313-
@doc false
314-
defp deep_stringify_keys(term) when is_map(term) do
315-
term
316-
|> Enum.map(fn {k, v} -> {to_string(k), deep_stringify_keys(v)} end)
317-
|> Enum.into(%{})
318-
end
319-
320-
defp deep_stringify_keys(term) when is_keyword_list(term) do
321-
term
322-
|> Enum.map(fn {k, v} -> {to_string(k), deep_stringify_keys(v)} end)
323-
|> Enum.into(%{})
324-
end
325-
326-
defp deep_stringify_keys(term) when is_list(term), do: Enum.map(term, &deep_stringify_keys/1)
327-
defp deep_stringify_keys(term), do: term
328-
329267
@doc false
330268
@spec post!(binary(), map(), headers()) :: {:ok, response()} | {:error, response() | term()}
331269
defp post!(path, %{} = body, headers) do
@@ -353,13 +291,4 @@ defmodule Posthog.Client do
353291
@spec encode(term(), module()) :: iodata()
354292
defp encode(data, Jason), do: Jason.encode_to_iodata!(data)
355293
defp encode(data, library), do: library.encode!(data)
356-
357-
@doc false
358-
@spec lib_properties() :: map()
359-
defp lib_properties do
360-
%{
361-
"$lib" => @lib_name,
362-
"$lib_version" => @lib_version
363-
}
364-
end
365294
end

lib/posthog/event.ex

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
defmodule Posthog.Event do
2+
@moduledoc """
3+
Represents a PostHog event with all its properties and metadata.
4+
5+
This struct encapsulates all the information needed to send an event to PostHog,
6+
including the event name, distinct ID, properties, timestamp, and UUID.
7+
8+
## Examples
9+
10+
# Create a basic event
11+
iex> event = Posthog.Event.new("page_view", "user_123")
12+
iex> event.event
13+
"page_view"
14+
iex> event.distinct_id
15+
"user_123"
16+
iex> event.properties
17+
%{}
18+
iex> is_binary(event.uuid)
19+
true
20+
iex> is_binary(event.timestamp)
21+
true
22+
23+
# Create an event with properties
24+
iex> event = Posthog.Event.new("purchase", "user_123", %{price: 99.99})
25+
iex> event.properties
26+
%{price: 99.99}
27+
28+
# Create an event with custom timestamp
29+
iex> timestamp = "2023-01-01T00:00:00Z"
30+
iex> event = Posthog.Event.new("login", "user_123", %{}, timestamp: timestamp)
31+
iex> event.timestamp
32+
"2023-01-01T00:00:00Z"
33+
34+
# Create an event with custom UUID
35+
iex> uuid = "123e4567-e89b-12d3-a456-426614174000"
36+
iex> event = Posthog.Event.new("signup", "user_123", %{}, uuid: uuid)
37+
iex> event.uuid
38+
"123e4567-e89b-12d3-a456-426614174000"
39+
40+
# Convert event to API payload
41+
iex> event = Posthog.Event.new("page_view", "user_123", %{page: "home"})
42+
iex> payload = Posthog.Event.to_api_payload(event)
43+
iex> payload.event
44+
"page_view"
45+
iex> payload.distinct_id
46+
"user_123"
47+
iex> payload.properties["page"]
48+
"home"
49+
iex> payload.properties["$lib"]
50+
"posthog-elixir"
51+
iex> is_binary(payload.uuid)
52+
true
53+
iex> is_binary(payload.timestamp)
54+
true
55+
56+
# Create batch payload
57+
iex> events = [
58+
...> Posthog.Event.new("page_view", "user_123", %{page: "home"}),
59+
...> Posthog.Event.new("click", "user_123", %{button: "signup"})
60+
...> ]
61+
iex> batch = Posthog.Event.batch_payload(events)
62+
iex> length(batch.batch)
63+
2
64+
iex> [first, second] = batch.batch
65+
iex> first.event
66+
"page_view"
67+
iex> second.event
68+
"click"
69+
"""
70+
71+
@type t :: %__MODULE__{
72+
event: String.t(),
73+
distinct_id: String.t(),
74+
properties: map(),
75+
uuid: String.t(),
76+
timestamp: String.t()
77+
}
78+
79+
@type event_name :: atom() | String.t()
80+
@type distinct_id :: String.t()
81+
@type properties :: map()
82+
@type timestamp :: String.t() | DateTime.t() | NaiveDateTime.t()
83+
84+
import Posthog.Guard, only: [is_keyword_list: 1]
85+
86+
defstruct [:event, :distinct_id, :properties, :uuid, :timestamp]
87+
88+
@lib_name "posthog-elixir"
89+
@lib_version Mix.Project.config()[:version]
90+
91+
@doc """
92+
Creates a new PostHog event.
93+
94+
## Parameters
95+
96+
* `event` - The name of the event (string or atom)
97+
* `distinct_id` - The distinct ID for the person or group
98+
* `properties` - Event properties (optional, defaults to empty map)
99+
* `timestamp` - Optional timestamp for the event (defaults to current UTC time)
100+
* `uuid` - Optional UUID for the event (defaults to a new UUID7)
101+
102+
## Examples
103+
104+
# Basic event
105+
Posthog.Event.new("page_view", "user_123")
106+
107+
# Event with properties
108+
Posthog.Event.new("purchase", "user_123", %{price: 99.99})
109+
110+
# Event with custom timestamp
111+
Posthog.Event.new("login", "user_123", %{}, timestamp: DateTime.utc_now())
112+
"""
113+
@spec new(event_name(), distinct_id(), properties(), keyword()) :: t()
114+
def new(event, distinct_id, properties \\ %{}, opts \\ []) do
115+
timestamp =
116+
Keyword.get_lazy(opts, :timestamp, fn ->
117+
DateTime.utc_now() |> DateTime.to_iso8601()
118+
end)
119+
120+
uuid = Keyword.get(opts, :uuid) || Uniq.UUID.uuid7()
121+
122+
%__MODULE__{
123+
event: to_string(event),
124+
distinct_id: distinct_id,
125+
properties: properties,
126+
uuid: uuid,
127+
timestamp: timestamp
128+
}
129+
end
130+
131+
@doc """
132+
Converts the event struct to a map suitable for sending to the PostHog API.
133+
"""
134+
@spec to_api_payload(t()) :: map()
135+
def to_api_payload(%__MODULE__{} = event) do
136+
%{
137+
event: event.event,
138+
distinct_id: event.distinct_id,
139+
properties: deep_stringify_keys(Map.merge(lib_properties(), Map.new(event.properties))),
140+
uuid: event.uuid,
141+
timestamp: event.timestamp
142+
}
143+
end
144+
145+
@doc """
146+
Creates a batch payload from a list of events.
147+
"""
148+
@spec batch_payload([t()]) :: map()
149+
def batch_payload(events) do
150+
%{batch: Enum.map(events, &to_api_payload/1)}
151+
end
152+
153+
@doc false
154+
defp deep_stringify_keys(term) when is_map(term) do
155+
term
156+
|> Enum.map(fn {k, v} -> {to_string(k), deep_stringify_keys(v)} end)
157+
|> Enum.into(%{})
158+
end
159+
160+
defp deep_stringify_keys(term) when is_keyword_list(term) do
161+
term
162+
|> Enum.map(fn {k, v} -> {to_string(k), deep_stringify_keys(v)} end)
163+
|> Enum.into(%{})
164+
end
165+
166+
defp deep_stringify_keys(term) when is_list(term), do: Enum.map(term, &deep_stringify_keys/1)
167+
defp deep_stringify_keys(term), do: term
168+
169+
@doc false
170+
defp lib_properties do
171+
%{
172+
"$lib" => @lib_name,
173+
"$lib_version" => @lib_version
174+
}
175+
end
176+
end

0 commit comments

Comments
 (0)