Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/call-flags-project-board.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ jobs:
is_draft: ${{ github.event.pull_request.draft }}
# The reusable workflow is pinned to a trusted PostHog/.github commit and needs
# the project-board bot secrets from this repository.
secrets: inherit # nosemgrep: yaml.github-actions.security.secrets-inherit.secrets-inherit
secrets: inherit # nosemgrep: yaml.github-actions.security.secrets-inherit.secrets-inherit -- reusable workflow requires inherited secrets
4 changes: 2 additions & 2 deletions .github/workflows/sdk-compliance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ on:
jobs:
compliance:
name: PostHog SDK compliance tests
uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@68498bcf3ae95ac941476e6ca3d42d6086e1c7fd
uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@02c049e529001d02f37a534745678e057d371fb0
with:
adapter-dockerfile: "sdk_compliance_adapter/Dockerfile"
adapter-context: "."
test-harness-version: "0.9.0"
test-harness-version: "0.10.0"
8 changes: 7 additions & 1 deletion lib/posthog/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ defmodule PostHog.API do
end

def flags(%__MODULE__.Client{} = client, event) do
client.module.request(client.client, :post, "/flags", json: event, params: %{v: 2})
client.module.request(client.client, :post, "/flags",
json: event,
params: %{v: 2},
retry: &__MODULE__.Client.retry_flags_request?/2,
retry_delay: &__MODULE__.Client.flags_retry_delay/1,
max_retries: client.feature_flags_request_max_retries
)
end
end
22 changes: 20 additions & 2 deletions lib/posthog/api/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ defmodule PostHog.API.Client do
"""
@behaviour __MODULE__

defstruct [:client, :module]
defstruct [:client, :module, feature_flags_request_max_retries: 1]

@typedoc """
Wrapper returned by `c:client/2` and stored in `t:PostHog.Config.config/0`.
Expand All @@ -106,7 +106,8 @@ defmodule PostHog.API.Client do
"""
@type t() :: %__MODULE__{
client: client(),
module: atom()
module: atom(),
feature_flags_request_max_retries: non_neg_integer()
}

@typedoc """
Expand Down Expand Up @@ -142,12 +143,29 @@ defmodule PostHog.API.Client do
# server-side; without it, flags gated to the server runtime are omitted
# from /flags responses.
@user_agent PostHog.Lib.user_agent()
@flags_retry_http_statuses [502, 504]

@doc """
The User-Agent header value sent with every API request.
"""
def user_agent, do: @user_agent

@doc false
def retry_flags_request?(_request, %Req.Response{status: status})
when status in @flags_retry_http_statuses,
do: true

def retry_flags_request?(_request, %Req.Response{}), do: false

def retry_flags_request?(_request, %Req.TransportError{}), do: true

def retry_flags_request?(_request, %Req.HTTPError{}), do: false

def retry_flags_request?(_request, _exception), do: false

@doc false
def flags_retry_delay(retry_count), do: trunc(300 * :math.pow(2, retry_count))

@impl __MODULE__
def client(api_key, api_host) do
client =
Expand Down
7 changes: 7 additions & 0 deletions lib/posthog/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ defmodule PostHog.Config do
default: PostHog.API.Client,
doc: "API client to use"
],
feature_flags_request_max_retries: [
type: :non_neg_integer,
default: 1,
doc:
"Number of retries for /flags requests after network, transport, or timeout failures. Set to 0 to disable retries."
],
supervisor_name: [
type: :atom,
default: PostHog,
Expand Down Expand Up @@ -227,6 +233,7 @@ defmodule PostHog.Config do
nil
else
config.api_client_module.client(config.api_key, config.api_host)
|> Map.put(:feature_flags_request_max_retries, config.feature_flags_request_max_retries)
end

system_global_properties =
Expand Down
2 changes: 1 addition & 1 deletion sdk_compliance_adapter/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ docker run -d --name sdk-adapter --network test-network -p 8080:8080 posthog-eli
docker run --rm \
--name test-harness \
--network test-network \
ghcr.io/posthog/sdk-test-harness:0.9.0 \
ghcr.io/posthog/sdk-test-harness:0.10.0 \
run --adapter-url http://sdk-adapter:8080 --mock-url http://test-harness:8081

# Cleanup
Expand Down
2 changes: 1 addition & 1 deletion sdk_compliance_adapter/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ services:

# Test harness
test-harness:
image: ghcr.io/posthog/sdk-test-harness:0.9.0
image: ghcr.io/posthog/sdk-test-harness:0.10.0
command: ["run", "--adapter-url", "http://sdk-adapter:8080", "--mock-url", "http://test-harness:8081"]
networks:
- test-network
Expand Down
4 changes: 2 additions & 2 deletions sdk_compliance_adapter/lib/sdk_compliance_adapter/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@ defmodule SdkComplianceAdapter.Router do
}

config = SdkComplianceAdapter.State.get_config()
flags_body = Map.put(body, :api_key, config[:api_key])
api_client = PostHog.config(SdkComplianceAdapter.PostHog).api_client

case Req.post("#{config[:api_host]}/flags/?v=2", json: flags_body) do
case PostHog.API.flags(api_client, body) do
{:ok, %{status: 200, body: resp_body}} ->
flags = Map.get(resp_body, "featureFlags") || Map.get(resp_body, "flags") || %{}
value = extract_flag_value(flags, key)
Expand Down
25 changes: 25 additions & 0 deletions test/posthog/api/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ defmodule PostHog.API.ClientTest do
assert req.headers["user-agent"] == [Client.user_agent()]
end

for {case_name, response_or_exception, expected} <- [
{"transport timeout", %Req.TransportError{reason: :timeout}, true},
{"transport closed", %Req.TransportError{reason: :closed}, true},
{"contract http status 502", %Req.Response{status: 502}, true},
{"contract http status 504", %Req.Response{status: 504}, true},
{"http error", %Req.HTTPError{reason: :closed}, false},
{"http status 408", %Req.Response{status: 408}, false},
{"http status 429", %Req.Response{status: 429}, false},
{"http status 500", %Req.Response{status: 500}, false},
{"http status 503", %Req.Response{status: 503}, false}
] do
test "flags retry policy handles #{case_name}" do
assert Client.retry_flags_request?(
%Req.Request{},
unquote(Macro.escape(response_or_exception))
) == unquote(expected)
end
end

for {retry_count, expected_delay} <- [{0, 300}, {1, 600}, {2, 1200}] do
test "flags retry delay for retry count #{retry_count}" do
assert Client.flags_retry_delay(unquote(retry_count)) == unquote(expected_delay)
end
end

test "request fallback continues uncompressed when compression step raises" do
parent = self()

Expand Down
15 changes: 15 additions & 0 deletions test/posthog/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ defmodule PostHog.ConfigTest do
assert config.api_host == "https://us.i.posthog.com"
end

test "validate applies feature flag retry count to the api client" do
expect(PostHog.API.Mock, :client, fn _api_key, _api_host ->
%PostHog.API.Client{client: :stub_client, module: PostHog.API.Mock}
end)

assert {:ok, config} =
PostHog.Config.validate(
api_key: "project_api_key",
api_client_module: PostHog.API.Mock,
feature_flags_request_max_retries: 0
)

assert config.api_client.feature_flags_request_max_retries == 0
end

test "validate defaults a blank api_host after trimming whitespace" do
expect(PostHog.API.Mock, :client, fn api_key, api_host ->
assert api_key == "project_api_key"
Expand Down
Loading