Skip to content

Commit 1ea4a4e

Browse files
feat: Add custom HTTP Client implementation
1 parent 08e1c5d commit 1ea4a4e

File tree

4 files changed

+164
-33
lines changed

4 files changed

+164
-33
lines changed

README.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ A powerful Elixir client for [PostHog](https://posthog.com), providing seamless
1313
- Custom Properties: Support for user, group, and person properties
1414
- Flexible Configuration: Customizable JSON library and API version
1515
- Environment Control: Disable tracking in development/test environments
16+
- Configurable HTTP Client: Customizable timeouts, retries, and HTTP client implementation
1617

1718
## Installation
1819

@@ -76,7 +77,49 @@ config :posthog,
7677
# Optional configurations
7778
config :posthog,
7879
json_library: Jason, # Default JSON parser (optional)
79-
enabled: true # Whether to enable PostHog tracking (optional, defaults to true)
80+
enabled: true, # Whether to enable PostHog tracking (optional, defaults to true)
81+
http_client: Posthog.HTTPClient.Hackney, # Default HTTP client (optional)
82+
http_client_opts: [ # HTTP client options (optional)
83+
timeout: 5_000, # Request timeout in milliseconds (default: 5_000)
84+
retries: 3, # Number of retries on failure (default: 3)
85+
retry_delay: 1_000 # Delay between retries in milliseconds (default: 1_000)
86+
]
87+
```
88+
89+
### HTTP Client Configuration
90+
91+
The library uses Hackney as the default HTTP client, but you can configure its behavior or even swap it for a different implementation by simply implementing the `Posthog.HTTPClient` behavior:
92+
93+
```elixir
94+
# config/config.exs
95+
config :posthog,
96+
# Use a different HTTP client implementation
97+
http_client: MyCustomHTTPClient,
98+
99+
# Configure HTTP client options
100+
http_client_opts: [
101+
timeout: 10_000, # 10 seconds timeout
102+
retries: 5, # 5 retries
103+
retry_delay: 2_000 # 2 seconds between retries
104+
]
105+
```
106+
107+
For testing, you might want to use a mock HTTP client:
108+
109+
```elixir
110+
# test/support/mocks.ex
111+
defmodule Posthog.HTTPClient.Test do
112+
@behaviour Posthog.HTTPClient
113+
114+
def post(url, body, headers, _opts) do
115+
# Return mock responses for testing
116+
{:ok, %{status: 200, headers: [], body: %{}}}
117+
end
118+
end
119+
120+
# config/test.exs
121+
config :posthog,
122+
http_client: Posthog.HTTPClient.Test
80123
```
81124

82125
### Disabling PostHog capture
@@ -248,7 +291,6 @@ If you encounter WX library issues during Erlang installation:
248291
```sh
249292
# Disable WX during installation
250293
export KERL_CONFIGURE_OPTIONS="--without-wx"
251-
asdf install
252294
```
253295

254296
To persist this setting, add it to your shell configuration file (`~/.bashrc`, `~/.zshrc`, or `~/.profile`).

lib/posthog/client.ex

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,13 @@ defmodule Posthog.Client do
1919
api_url: "https://app.posthog.com", # Required
2020
api_key: "phc_your_project_api_key", # Required
2121
json_library: Jason, # Optional (default: Jason)
22-
enabled_capture: true # Optional (default: true)
23-
# Set to false to disable all tracking
22+
enabled_capture: true, # Optional (default: true)
23+
http_client: Posthog.HTTPClient.Hackney, # Optional (default: Posthog.HTTPClient.Hackney)
24+
http_client_opts: [ # Optional
25+
timeout: 5_000, # 5 seconds
26+
retries: 3, # Number of retries
27+
retry_delay: 1_000 # 1 second between retries
28+
]
2429
2530
### Disabling capture
2631
@@ -365,35 +370,12 @@ defmodule Posthog.Client do
365370

366371
url = Posthog.Config.api_url() <> path
367372

368-
:hackney.post(url, headers, body, [])
369-
|> handle()
370-
end
371-
372-
@doc false
373-
@spec handle(tuple()) :: {:ok, response()} | {:error, response() | term()}
374-
defp handle({:ok, status, _headers, _ref} = resp) when div(status, 100) == 2 do
375-
{:ok, to_response(resp)}
376-
end
377-
378-
defp handle({:ok, _status, _headers, _ref} = resp) do
379-
{:error, to_response(resp)}
380-
end
381-
382-
defp handle({:error, _} = result) do
383-
result
384-
end
385-
386-
@doc false
387-
@spec to_response({:ok, pos_integer(), headers(), reference()}) :: response()
388-
defp to_response({_, status, headers, ref}) do
389-
response = %{status: status, headers: headers, body: nil}
390-
391-
with {:ok, body} <- :hackney.body(ref),
392-
{:ok, json} <- Posthog.Config.json_library().decode(body) do
393-
%{response | body: json}
394-
else
395-
_ -> response
396-
end
373+
Posthog.Config.http_client().post(
374+
url,
375+
body,
376+
headers,
377+
Posthog.Config.http_client_opts()
378+
)
397379
end
398380

399381
@doc false

lib/posthog/config.ex

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,31 @@ defmodule Posthog.Config do
8484
Application.get_env(@app, :json_library, Jason)
8585
end
8686

87+
@doc """
88+
Returns the HTTP client module to use.
89+
90+
Defaults to Posthog.HTTPClient.Hackney if not configured.
91+
"""
92+
def http_client do
93+
Application.get_env(@app, :http_client, Posthog.HTTPClient.Hackney)
94+
end
95+
96+
@doc """
97+
Returns the HTTP client options.
98+
99+
Defaults to:
100+
- timeout: 5_000 (5 seconds)
101+
- retries: 3
102+
- retry_delay: 1_000 (1 second)
103+
"""
104+
def http_client_opts do
105+
Application.get_env(@app, :http_client_opts,
106+
timeout: 5_000,
107+
retries: 3,
108+
retry_delay: 1_000
109+
)
110+
end
111+
87112
@doc """
88113
Validates the entire PostHog configuration at compile time.
89114
@@ -109,6 +134,20 @@ defmodule Posthog.Config do
109134
end
110135
end
111136

137+
# Validate HTTP client
138+
http_client = http_client()
139+
140+
unless Code.ensure_loaded?(http_client) do
141+
raise """
142+
Configured HTTP client #{inspect(http_client)} is not available.
143+
144+
Make sure to add it to your dependencies in mix.exs:
145+
defp deps do
146+
[{:hackney, "~> x.x"}]
147+
end
148+
"""
149+
end
150+
112151
:ok
113152
end
114153
end

lib/posthog/http_client.ex

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
defmodule Posthog.HTTPClient do
2+
@moduledoc """
3+
Behaviour for HTTP client implementations.
4+
5+
This allows for easy swapping of HTTP clients and better testability.
6+
"""
7+
8+
@type headers :: [{binary(), binary()}]
9+
@type response :: %{status: pos_integer(), headers: headers(), body: map() | nil}
10+
@type body :: iodata() | binary()
11+
@type url :: binary()
12+
13+
@doc """
14+
Makes a POST request to the given URL with the specified body and headers.
15+
16+
Returns `{:ok, response}` on success or `{:error, reason}` on failure.
17+
"""
18+
@callback post(url(), body(), headers(), keyword()) :: {:ok, response()} | {:error, term()}
19+
end
20+
21+
defmodule Posthog.HTTPClient.Hackney do
22+
@moduledoc """
23+
Hackney-based implementation of the Posthog.HTTPClient behaviour.
24+
"""
25+
26+
@behaviour Posthog.HTTPClient
27+
28+
@default_timeout 5_000
29+
@default_retries 3
30+
@default_retry_delay 1_000
31+
32+
@impl true
33+
def post(url, body, headers, opts \\ []) do
34+
timeout = Keyword.get(opts, :timeout, @default_timeout)
35+
retries = Keyword.get(opts, :retries, @default_retries)
36+
retry_delay = Keyword.get(opts, :retry_delay, @default_retry_delay)
37+
38+
do_post(url, body, headers, timeout, retries, retry_delay)
39+
end
40+
41+
defp do_post(url, body, headers, timeout, retries, retry_delay) do
42+
case :hackney.post(url, headers, body, []) do
43+
{:ok, status, _headers, _ref} = resp when div(status, 100) == 2 ->
44+
{:ok, to_response(resp)}
45+
46+
{:ok, _status, _headers, _ref} = resp ->
47+
{:error, to_response(resp)}
48+
49+
{:error, _reason} when retries > 0 ->
50+
Process.sleep(retry_delay)
51+
do_post(url, body, headers, timeout, retries - 1, retry_delay)
52+
53+
{:error, _reason} = error ->
54+
error
55+
end
56+
end
57+
58+
defp to_response({_, status, headers, ref}) do
59+
response = %{status: status, headers: headers, body: nil}
60+
61+
with {:ok, body} <- :hackney.body(ref),
62+
{:ok, json} <- Posthog.Config.json_library().decode(body) do
63+
%{response | body: json}
64+
else
65+
_ -> response
66+
end
67+
end
68+
end

0 commit comments

Comments
 (0)