Skip to content

Commit bcd8d29

Browse files
feat: Add gzip compression turned on by default (#84)
This is *technically* a breaking change because we're now always sending data gzip compressed and people might not want that, but this will not break anyone's code so we'll release it as a minor knowing that it's an improvement. It's always been possible to swap the client off, but we weren't documenting how to do that exactly - this is now solved too.
2 parents 12ac913 + 0f1f78e commit bcd8d29

File tree

4 files changed

+137
-6
lines changed

4 files changed

+137
-6
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
hex/posthog: minor
3+
---
4+
5+
This is *technically* a breaking change because we're now always sending data gzip compressed and people might not want that, but this will not break anyone's code so we'll release it as a minor knowing that it's an improvement. It's always been possible to swap the client off, but we weren't documenting how to do that exactly - this is now solved too.

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,44 @@ You can always disable it by setting `enable_error_tracking` to false:
210210
config :posthog, enable_error_tracking: false
211211
```
212212

213+
## Custom HTTP Client
214+
215+
The SDK uses [Req](https://hexdocs.pm/req) under the hood with gzip compression and
216+
transient retry enabled by default. You can swap in your own HTTP client module to
217+
change any of this behaviour — disable compression, add custom headers, attach
218+
telemetry, or use a completely different HTTP library.
219+
220+
Set the `api_client_module` config option to a module that implements the
221+
`PostHog.API.Client` behaviour:
222+
223+
```elixir
224+
config :posthog, api_client_module: MyApp.PostHogClient
225+
```
226+
227+
The simplest approach is to wrap the default client and override only what you need:
228+
229+
```elixir
230+
defmodule MyApp.PostHogClient do
231+
@behaviour PostHog.API.Client
232+
233+
@impl true
234+
def client(api_key, api_host) do
235+
default = PostHog.API.Client.client(api_key, api_host)
236+
237+
# Disable gzip compression
238+
custom = Req.merge(default.client, compress_body: false)
239+
240+
%{default | client: custom}
241+
end
242+
243+
@impl true
244+
defdelegate request(client, method, url, opts), to: PostHog.API.Client
245+
end
246+
```
247+
248+
See `PostHog.API.Client` docs for more examples, including adding custom headers
249+
and using a different HTTP library entirely.
250+
213251
## Multiple PostHog Projects
214252

215253
If your app works with multiple PostHog projects, PostHog can accommodate you. For
@@ -276,6 +314,7 @@ sampo add
276314
```
277315

278316
Follow the prompts to specify:
317+
279318
- The type of change (`patch`, `minor`, or `major`)
280319
- A description of the change for the changelog
281320

@@ -293,6 +332,7 @@ the [PostHog SDK releases process](https://posthog.com/handbook/engineering/sdks
293332
3. **Merge the PR** into `master`
294333
295334
Once merged, the release workflow will automatically:
335+
296336
- Consume all pending changesets
297337
- Update the version in `mix.exs`
298338
- Update `CHANGELOG.md` with the new entries

lib/posthog/api/client.ex

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ defmodule PostHog.API.Client do
22
@moduledoc """
33
Behaviour and the default implementation of a PostHog API client. Uses `Req`.
44
5-
Users are unlikely to interact with this module directly, but here's an
6-
example just in case:
5+
The default client sends request bodies gzip-compressed and retries on transient
6+
failures. If you need different behaviour — for example, to disable compression,
7+
add custom headers, attach telemetry, or use a different HTTP library — you can
8+
implement this behaviour in your own module and configure it via `api_client_module`.
79
8-
## Example
10+
## Using the default client directly
911
1012
> client = PostHog.API.Client.client("phc_abcdedfgh", "https://us.i.posthog.com")
1113
%PostHog.API.Client{
@@ -15,6 +17,82 @@ defmodule PostHog.API.Client do
1517
1618
> client.module.request(client.client, :post, "/flags", json: %{distinct_id: "user123"}, params: %{v: 2, config: true})
1719
{:ok, %Req.Response{status: 200, body: %{...}}}
20+
21+
## Writing a custom client
22+
23+
Implement the `client/2` and `request/4` callbacks, then set the config:
24+
25+
config :posthog, api_client_module: MyApp.PostHogClient
26+
27+
### Wrapping the default client
28+
29+
The easiest approach is to delegate to the default client and override only what
30+
you need. For example, to disable gzip compression:
31+
32+
defmodule MyApp.PostHogClient do
33+
@behaviour PostHog.API.Client
34+
35+
@impl true
36+
def client(api_key, api_host) do
37+
default = PostHog.API.Client.client(api_key, api_host)
38+
# Remove the compress_body step added by the default client
39+
custom = Req.merge(default.client, compress_body: false)
40+
%{default | client: custom}
41+
end
42+
43+
@impl true
44+
defdelegate request(client, method, url, opts), to: PostHog.API.Client
45+
end
46+
47+
### Adding custom request headers
48+
49+
defmodule MyApp.PostHogClient do
50+
@behaviour PostHog.API.Client
51+
52+
@impl true
53+
def client(api_key, api_host) do
54+
default = PostHog.API.Client.client(api_key, api_host)
55+
custom = Req.merge(default.client, headers: [{"x-custom-header", "value"}])
56+
%{default | client: custom}
57+
end
58+
59+
@impl true
60+
defdelegate request(client, method, url, opts), to: PostHog.API.Client
61+
end
62+
63+
### Using a different HTTP library
64+
65+
You can skip `Req` entirely and use any HTTP client. The `client` term you return
66+
is opaque — it's passed back to your `request/4` callback as-is.
67+
68+
NOTE: The code below is not guaranteed to be correct or complete — it's just illustrative of the general approach.
69+
70+
defmodule MyApp.FinchPostHogClient do
71+
@behaviour PostHog.API.Client
72+
73+
@impl true
74+
def client(api_key, api_host) do
75+
%PostHog.API.Client{
76+
client: %{api_key: api_key, api_host: api_host},
77+
module: __MODULE__
78+
}
79+
end
80+
81+
@impl true
82+
def request(client, method, url, opts) do
83+
body = opts[:json] |> Map.put_new(:api_key, client.api_key) |> Jason.encode!()
84+
85+
Finch.build(method, client.api_host <> url, [{"content-type", "application/json"}], body)
86+
|> Finch.request(MyApp.Finch)
87+
|> case do
88+
{:ok, %Finch.Response{status: status, body: body}} ->
89+
{:ok, %{status: status, body: Jason.decode!(body)}}
90+
91+
{:error, exception} ->
92+
{:error, exception}
93+
end
94+
end
95+
end
1896
"""
1997
@behaviour __MODULE__
2098

@@ -48,7 +126,7 @@ defmodule PostHog.API.Client do
48126
@impl __MODULE__
49127
def client(api_key, api_host) do
50128
client =
51-
Req.new(base_url: api_host, retry: :transient)
129+
Req.new(base_url: api_host, retry: :transient, compress_body: true)
52130
|> Req.Request.put_private(:api_key, api_key)
53131

54132
%__MODULE__{client: client, module: __MODULE__}

sdk_compliance_adapter/lib/sdk_compliance_adapter/tracked_client.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ defmodule SdkComplianceAdapter.TrackedClient do
2222
defdelegate request(client, method, url, opts), to: PostHog.API.Client
2323

2424
def track({request, response}) do
25-
req_body =
25+
req_body =
2626
request.body
2727
|> to_string()
28+
|> decompress(request)
2829
|> JSON.decode!()
29-
30+
3031
uuid_list = extract_uuids(req_body)
3132
event_count = count_events(req_body)
3233

@@ -40,6 +41,13 @@ defmodule SdkComplianceAdapter.TrackedClient do
4041
{request, exception}
4142
end
4243

44+
defp decompress(body, request) do
45+
case Req.Request.get_header(request, "content-encoding") do
46+
["gzip"] -> :zlib.gunzip(body)
47+
_ -> body
48+
end
49+
end
50+
4351
defp extract_uuids(request) do
4452
request
4553
|> get_in([Access.key("batch", []), Access.all(), "uuid"])

0 commit comments

Comments
 (0)