Skip to content

Commit d288bf2

Browse files
committed
docs: document public API ExDoc comments
1 parent 32d6d3e commit d288bf2

14 files changed

Lines changed: 298 additions & 92 deletions

File tree

README.md

Lines changed: 57 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -113,91 +113,89 @@ You can always inspect the context:
113113
iex> PostHog.get_context()
114114
%{distinct_id: "distinct_id_of_the_user"}
115115
iex> PostHog.get_event_context("sensitive_event")
116-
%{distinct_id: "distinct_id_of_the_user", "$process_person_profile": true}
116+
%{distinct_id: "distinct_id_of_the_user", "$process_person_profile": false}
117117
```
118118

119119
## Feature Flags
120120

121-
`PostHog.FeatureFlags.check/2` is the main function for checking a feature flag.
121+
Evaluate feature flags once for a user with `PostHog.FeatureFlags.evaluate_flags/1`,
122+
then read values from the returned snapshot.
122123

123124
```elixir
124-
# Simple boolean feature flag
125-
iex> PostHog.FeatureFlags.check("example-feature-flag-1", "user123")
126-
{:ok, true}
127-
128-
# Note how it automatically sets `$feature/example-feature-flag-1` property in the context
129-
iex> PostHog.get_context()
130-
%{"$feature/example-feature-flag-1" => true}
131-
132-
# It will attempt to take distinct_id from the context if it's not provided
133-
iex> PostHog.set_context(%{distinct_id: "user123"})
134-
:ok
135-
iex> PostHog.FeatureFlags.check("example-feature-flag-1")
136-
{:ok, true}
137-
138-
# You can also pass a map with body parameters that will be sent to the /flags API as-is
139-
iex> PostHog.FeatureFlags.check("example-feature-flag-1", %{distinct_id: "user123", groups: %{group_type: "group_id"}})
140-
{:ok, true}
125+
# Boolean feature flag
126+
{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("user123")
127+
if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "new-dashboard") do
128+
# Do something differently for this user
129+
end
141130

142-
# It returns variant if it's set
143-
iex> PostHog.FeatureFlags.check("example-feature-flag-2", "user123")
144-
{:ok, "variant2"}
131+
# Multivariate feature flag
132+
case PostHog.FeatureFlags.Evaluations.get_flag(snapshot, "checkout-flow") do
133+
"variant-a" -> :variant_a
134+
true -> :enabled_boolean_flag
135+
false -> :disabled
136+
nil -> :not_returned
137+
end
145138

146-
# Returns error if feature flag doesn't exist
147-
iex> PostHog.FeatureFlags.check("example-feature-flag-3", "user123")
148-
{:error, %PostHog.UnexpectedResponseError{message: "Feature flag example-feature-flag-3 was not found in the response", response: ...}}
139+
# Optional payload
140+
payload = PostHog.FeatureFlags.Evaluations.get_flag_payload(snapshot, "checkout-flow")
149141
```
150142

151-
If you're feeling adventurous and/or simply writing a script, you can use the `PostHog.FeatureFlags.check!/2` helper instead and it will return a boolean or raise an error.
152-
153-
```elixir
154-
# Simple boolean feature flag
155-
iex> PostHog.FeatureFlags.check!("example-feature-flag-1", "user123")
156-
true
157-
158-
# Works for variants too
159-
iex> PostHog.FeatureFlags.check!("example-feature-flag-2", "user123")
160-
"variant2"
161-
162-
# Raises error if feature flag doesn't exist
163-
iex> PostHog.FeatureFlags.check!("example-feature-flag-3", "user123")
164-
** (PostHog.UnexpectedResponseError) Feature flag example-feature-flag-3 was not found in the response
165-
```
143+
`get_flag/2` returns the variant string for multivariate flags, `true` for enabled
144+
boolean flags, `false` for disabled flags, and `nil` when the flag was not returned
145+
by the evaluation.
166146

167-
### Getting the Full Flag Result
147+
### Include feature flag information when capturing events
168148

169-
If you need more than just the value -- for example, the payload configured for a
170-
flag or variant -- use `PostHog.FeatureFlags.get_feature_flag_result/2`:
149+
If you want to break down or filter captured events by feature flag value, put the
150+
same snapshot in the process context before capturing events:
171151

172152
```elixir
173-
iex> PostHog.FeatureFlags.get_feature_flag_result("my-flag", "user123")
174-
{:ok, %PostHog.FeatureFlags.Result{key: "my-flag", enabled: true, variant: nil, payload: nil}}
153+
{:ok, snapshot} = PostHog.FeatureFlags.evaluate_flags("user123")
175154

176-
# Multivariant flag with a JSON payload
177-
iex> PostHog.FeatureFlags.get_feature_flag_result("my-experiment", "user123")
178-
{:ok, %PostHog.FeatureFlags.Result{key: "my-experiment", enabled: true, variant: "control", payload: %{"button_color" => "blue"}}}
155+
if PostHog.FeatureFlags.Evaluations.enabled?(snapshot, "new-dashboard") do
156+
# Do something differently for this user
157+
end
179158

180-
# Flag not found
181-
iex> PostHog.FeatureFlags.get_feature_flag_result("non-existent-flag", "user123")
182-
{:ok, nil}
159+
PostHog.FeatureFlags.set_in_context(snapshot)
160+
PostHog.capture("page_viewed", %{distinct_id: "user123"})
183161
```
184162

185-
By default this sends a `$feature_flag_called` event, which PostHog uses to
186-
track feature flag usage in your analytics, and to measure experiment exposure
187-
when the flag is linked to an A/B test. You can opt out with `send_event: false`:
163+
This attaches `$feature/<flag-key>` properties and `$active_feature_flags` without
164+
making another `/flags` request. To reduce event property bloat, filter the
165+
snapshot first:
188166

189167
```elixir
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}}
168+
# Attach only flags accessed with enabled?/2 or get_flag/2
169+
PostHog.FeatureFlags.set_in_context(
170+
PostHog.FeatureFlags.Evaluations.only_accessed(snapshot)
171+
)
172+
173+
# Or attach only specific flags
174+
PostHog.FeatureFlags.set_in_context(
175+
PostHog.FeatureFlags.Evaluations.only(snapshot, ["checkout-flow", "new-dashboard"])
176+
)
192177
```
193178

194-
A bang variant is also available:
179+
### Evaluating only specific flags
180+
181+
By default, `evaluate_flags/1` evaluates every flag for the user. If you only need
182+
a few flags, pass `:flag_keys` to request only those flags:
195183

196184
```elixir
197-
iex> PostHog.FeatureFlags.get_feature_flag_result!("my-flag", "user123")
198-
%PostHog.FeatureFlags.Result{key: "my-flag", enabled: true, variant: nil, payload: nil}
185+
{:ok, snapshot} =
186+
PostHog.FeatureFlags.evaluate_flags(%{
187+
distinct_id: "user123",
188+
flag_keys: ["checkout-flow", "new-dashboard"]
189+
})
199190
```
200191

192+
> #### Deprecated feature flag helpers {: .warning}
193+
>
194+
> `PostHog.FeatureFlags.check/2`, `PostHog.FeatureFlags.check!/2`,
195+
> `PostHog.FeatureFlags.get_feature_flag_result/2`, and
196+
> `PostHog.FeatureFlags.get_feature_flag_result!/2` still work during the
197+
> migration period, but prefer `evaluate_flags/1` for new code.
198+
201199
## Error Tracking
202200

203201
Error Tracking is enabled by default.

lib/posthog.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule PostHog do
99
@typedoc ~S(Event name, such as `"user_signed_up"` or `"$create_alias"`)
1010
@type event() :: String.t()
1111

12-
@typedoc "string representing distinct ID"
12+
@typedoc "String representing a PostHog distinct ID."
1313
@type distinct_id() :: String.t()
1414

1515
@typedoc """
@@ -194,6 +194,7 @@ defmodule PostHog do
194194
> PostHog.get_event_context(MyPostHog, "$exception")
195195
%{foo: "bar"}
196196
"""
197-
@spec get_event_context(supervisor_name()) :: properties()
197+
@spec get_event_context(event()) :: properties()
198+
@spec get_event_context(supervisor_name(), event()) :: properties()
198199
def get_event_context(name \\ __MODULE__, event), do: PostHog.Context.get(name, event)
199200
end

lib/posthog/api/client.ex

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,31 @@ defmodule PostHog.API.Client do
9898

9999
defstruct [:client, :module]
100100

101+
@typedoc """
102+
Wrapper returned by `c:client/2` and stored in `t:PostHog.Config.config/0`.
103+
104+
- `:client` - opaque client state passed back to `c:request/4`.
105+
- `:module` - module implementing this behaviour.
106+
"""
101107
@type t() :: %__MODULE__{
102108
client: client(),
103109
module: atom()
104110
}
111+
105112
@typedoc """
106113
Arbitrary term that is passed as the first argument to the `c:request/4` callback.
107114
108115
For the default client, this is a `t:Req.Request.t/0` struct.
109116
"""
110117
@type client() :: any()
118+
119+
@typedoc """
120+
Response tuple returned by `c:request/4`.
121+
122+
Successful responses must expose at least a numeric `:status` and decoded
123+
`:body`; errors should return the exception or error struct from the HTTP
124+
client.
125+
"""
111126
@type response() :: {:ok, %{status: non_neg_integer(), body: any()}} | {:error, Exception.t()}
112127

113128
@doc """
@@ -123,7 +138,22 @@ defmodule PostHog.API.Client do
123138
@callback request(client :: client(), method :: atom(), url :: String.t(), opts :: keyword()) ::
124139
response()
125140

141+
@doc """
142+
Creates the default Req-backed PostHog API client.
143+
144+
## Parameters
145+
146+
- `api_key` - PostHog project API key. It is stored privately and inserted
147+
into JSON request bodies by `request/4`.
148+
- `api_host` - PostHog ingestion host, such as `https://us.i.posthog.com`.
149+
150+
## Returns
151+
152+
Returns a `t:t/0` whose `:client` field is a configured `t:Req.Request.t/0`
153+
and whose `:module` field is `PostHog.API.Client`.
154+
"""
126155
@impl __MODULE__
156+
@spec client(String.t(), String.t()) :: t()
127157
def client(api_key, api_host) do
128158
client =
129159
Req.new(base_url: api_host, retry: :transient, compress_body: true)
@@ -132,7 +162,27 @@ defmodule PostHog.API.Client do
132162
%__MODULE__{client: client, module: __MODULE__}
133163
end
134164

165+
@doc """
166+
Sends an API request with the default Req-backed client.
167+
168+
## Parameters
169+
170+
- `client` - `t:client/0` returned by `client/2`.
171+
- `method` - HTTP method atom, for example `:post`.
172+
- `url` - path relative to the configured API host.
173+
- `opts` - Req options for the request.
174+
175+
## Returns
176+
177+
Returns `t:response/0`.
178+
179+
## Remarks
180+
181+
When `opts` contains a `:json` body, the configured API key is added to that
182+
body unless an `:api_key` is already present.
183+
"""
135184
@impl __MODULE__
185+
@spec request(client(), atom(), String.t(), keyword()) :: response()
136186
def request(client, method, url, opts) do
137187
client
138188
|> Req.merge(

lib/posthog/config.ex

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ defmodule PostHog.Config do
77
test_mode: [
88
type: :boolean,
99
default: false,
10-
doc: "Test mode allows tests assert captured events."
10+
doc:
11+
"Test mode drops events in memory so tests can assert captured events without sending them to PostHog."
1112
]
1213
]
1314

@@ -52,6 +53,21 @@ defmodule PostHog.Config do
5253
default: %{},
5354
doc: "Map of properties that should be added to all events"
5455
],
56+
sender_pool_size: [
57+
type: :pos_integer,
58+
doc:
59+
"Number of background sender workers used to batch and send events. Defaults to `max(System.schedulers_online(), 2)`."
60+
],
61+
max_batch_time_ms: [
62+
type: :non_neg_integer,
63+
doc:
64+
"Maximum time, in milliseconds, to wait before flushing a non-empty event batch. Defaults to `10_000`."
65+
],
66+
max_batch_events: [
67+
type: :pos_integer,
68+
doc:
69+
"Maximum number of events to collect before flushing a batch immediately. Defaults to `100`."
70+
],
5571
in_app_otp_apps: [
5672
type: {:list, :atom},
5773
default: [],
@@ -134,12 +150,21 @@ defmodule PostHog.Config do
134150
"""
135151

136152
@typedoc """
137-
Map containing valid configuration.
153+
Map containing validated configuration for a PostHog supervision tree.
138154
139-
It mostly follows `t:options/0`, but the internal structure shouldn't be relied upon.
155+
It mostly follows `t:options/0`, but also includes runtime values such as the
156+
initialized API client, resolved in-app modules, and system global properties.
157+
The internal structure should not be relied upon outside of starting
158+
`PostHog.Supervisor` or reading values through `PostHog.config/1`.
140159
"""
141160
@opaque config() :: map()
142161

162+
@typedoc """
163+
Keyword options accepted by `validate/1` and `validate!/1`.
164+
165+
See the module documentation for the full schema, defaults, and remarks for
166+
each configuration option.
167+
"""
143168
@type options() :: unquote(NimbleOptions.option_typespec(@compiled_configuration_schema))
144169

145170
@doc false
@@ -166,7 +191,9 @@ defmodule PostHog.Config do
166191
end
167192

168193
@doc """
169-
See `validate/1`.
194+
Validates configuration and returns a `t:config/0`, raising if validation fails.
195+
196+
See `validate/1` for the accepted options and return shape.
170197
"""
171198
@spec validate!(options()) :: config()
172199
def validate!(options) do
@@ -175,7 +202,21 @@ defmodule PostHog.Config do
175202
end
176203

177204
@doc """
178-
Validates configuration against the schema.
205+
Validates configuration against the supervisor schema.
206+
207+
## Parameters
208+
209+
- `options` - keyword list matching `t:options/0`.
210+
211+
## Returns
212+
213+
Returns `{:ok, config}` with a normalized `t:config/0` on success, or
214+
`{:error, %NimbleOptions.ValidationError{}}` when the options are invalid.
215+
216+
## Remarks
217+
218+
String `:api_key` and `:api_host` values are trimmed before validation. A blank
219+
`:api_host` falls back to the default PostHog US ingestion host.
179220
"""
180221
@spec validate(options()) ::
181222
{:ok, config()} | {:error, NimbleOptions.ValidationError.t()}

lib/posthog/error.ex

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
defmodule PostHog.Error do
22
@moduledoc """
3-
PostHog error
3+
Generic PostHog SDK error.
4+
5+
## Fields
6+
7+
- `:message` - human-readable error message.
48
"""
59

10+
@typedoc "Exception raised for SDK errors that are not tied to an HTTP response."
611
@type t() :: %__MODULE__{message: String.t()}
712

813
defexception [:message]
914
end
1015

1116
defmodule PostHog.UnexpectedResponseError do
1217
@moduledoc """
13-
PostHog error that includes a reponse from the API, either full or partial.
18+
PostHog error that includes a response from the API, either full or partial.
19+
20+
## Fields
21+
22+
- `:message` - human-readable error message.
23+
- `:response` - API response data that caused the error.
1424
"""
25+
26+
@typedoc "Exception raised when PostHog returns a response the SDK cannot handle."
1527
@type t() :: %__MODULE__{response: any(), message: String.t()}
1628

1729
defexception [:response, :message]

0 commit comments

Comments
 (0)