From ad9ab560be422a941409879c1cd012bf61e38d0b Mon Sep 17 00:00:00 2001 From: Anders Asheim Hennum Date: Tue, 28 Apr 2026 16:45:43 +0200 Subject: [PATCH 1/3] feat(compliance): declare capture_v0 and encoding_gzip capabilities --- sdk_compliance_adapter/lib/sdk_compliance_adapter/router.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk_compliance_adapter/lib/sdk_compliance_adapter/router.ex b/sdk_compliance_adapter/lib/sdk_compliance_adapter/router.ex index f1d8275..9930bd0 100644 --- a/sdk_compliance_adapter/lib/sdk_compliance_adapter/router.ex +++ b/sdk_compliance_adapter/lib/sdk_compliance_adapter/router.ex @@ -36,7 +36,8 @@ defmodule SdkComplianceAdapter.Router do response = %{ sdk_name: "posthog-elixir", sdk_version: @sdk_version, - adapter_version: @adapter_version + adapter_version: @adapter_version, + capabilities: ["capture_v0", "encoding_gzip"] } json_response(conn, 200, response) From 35e0beef139957f2647bde92d7c5f58663d89641 Mon Sep 17 00:00:00 2001 From: Anders Asheim Hennum Date: Tue, 28 Apr 2026 16:46:24 +0200 Subject: [PATCH 2/3] feat(flags): implement /get_feature_flag adapter endpoint The endpoint constructs the /flags request body the harness contract expects: distinct_id at the top level, distinct_id mirrored into person_properties (the auto-added value the contract asserts on), groups/group_properties defaulting to empty maps, geoip_disable, and flag_keys_to_evaluate scoped to the requested key. The body is then sent through PostHog.FeatureFlags.flags/2 so the SDK's API client auto-injects api_key and (when enabled) gzip-compresses the request. Returns the resolved variant string, the boolean enabled state for boolean flags, or null when the flag is missing from the response. This satisfies request_payload.request_with_person_properties_device_id in the feature_flags suite of the SDK Test Harness. --- .../lib/sdk_compliance_adapter/router.ex | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/sdk_compliance_adapter/lib/sdk_compliance_adapter/router.ex b/sdk_compliance_adapter/lib/sdk_compliance_adapter/router.ex index 9930bd0..0c1faa0 100644 --- a/sdk_compliance_adapter/lib/sdk_compliance_adapter/router.ex +++ b/sdk_compliance_adapter/lib/sdk_compliance_adapter/router.ex @@ -119,6 +119,58 @@ defmodule SdkComplianceAdapter.Router do json_response(conn, 200, response) end + # POST /get_feature_flag - Evaluate a feature flag + post "/get_feature_flag" do + params = conn.body_params + + key = params["key"] + distinct_id = params["distinct_id"] + + cond do + is_nil(key) -> + json_response(conn, 400, %{success: false, error: "Missing key"}) + + is_nil(distinct_id) -> + json_response(conn, 400, %{success: false, error: "Missing distinct_id"}) + + true -> + # Build the /flags request body. The PostHog Elixir SDK forwards the + # body to the /flags endpoint as-is (api_key is auto-injected by the + # API client). The harness asserts on the actual /flags HTTP request, + # so we mirror person_properties.distinct_id here per the contract: + # "auto-added distinct_id in person_properties". + person_properties = + (params["person_properties"] || %{}) + |> Map.put("distinct_id", distinct_id) + + body = + %{ + distinct_id: distinct_id, + person_properties: person_properties, + groups: params["groups"] || %{}, + group_properties: params["group_properties"] || %{}, + flag_keys_to_evaluate: [key] + } + |> maybe_put(:geoip_disable, params["disable_geoip"]) + + case PostHog.FeatureFlags.flags(SdkComplianceAdapter.PostHog, body) do + {:ok, %{status: 200, body: %{"flags" => flags}}} -> + value = extract_flag_value(flags, key) + json_response(conn, 200, %{success: true, value: value}) + + {:ok, %{status: status, body: resp_body}} -> + json_response(conn, 200, %{ + success: false, + error: "Unexpected response status: #{status}", + response: inspect(resp_body) + }) + + {:error, reason} -> + json_response(conn, 200, %{success: false, error: inspect(reason)}) + end + end + end + # POST /reset - Reset SDK state post "/reset" do stop_posthog() @@ -183,6 +235,27 @@ defmodule SdkComplianceAdapter.Router do end end + defp maybe_put(map, _key, nil), do: map + defp maybe_put(map, key, value), do: Map.put(map, key, value) + + defp extract_flag_value(flags, key) when is_map(flags) do + case Map.get(flags, key) do + nil -> + nil + + flag_data when is_map(flag_data) -> + cond do + is_binary(flag_data["variant"]) -> flag_data["variant"] + true -> Map.get(flag_data, "enabled", false) == true + end + + other -> + other + end + end + + defp extract_flag_value(_flags, _key), do: nil + defp stop_posthog do case Process.whereis(SdkComplianceAdapter.PostHog) do nil -> From 5b2a5016ef4a343f6f2f1ab4b7eb3ca7a63d23a9 Mon Sep 17 00:00:00 2001 From: Anders Asheim Hennum Date: Tue, 28 Apr 2026 16:46:38 +0200 Subject: [PATCH 3/3] docs(compliance): document /get_feature_flag endpoint and capabilities --- sdk_compliance_adapter/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/sdk_compliance_adapter/README.md b/sdk_compliance_adapter/README.md index ab3a018..8ba42ad 100644 --- a/sdk_compliance_adapter/README.md +++ b/sdk_compliance_adapter/README.md @@ -23,13 +23,20 @@ The adapter is a standalone Elixir application that: ### Endpoints -- `GET /health` - Health check, returns SDK name/version +- `GET /health` - Health check, returns SDK name/version and supported capabilities - `POST /init` - Initialize SDK with configuration - `POST /capture` - Capture a single event - `POST /flush` - Flush pending events +- `POST /get_feature_flag` - Evaluate a feature flag against the `/flags` API - `GET /state` - Get internal state for test assertions - `POST /reset` - Reset SDK state +### Capabilities + +The adapter declares `capture_v0` and `encoding_gzip` capabilities, which gates +the test suites the harness will run. The `feature_flags` suite has no +capability requirement and runs unconditionally. + ## Documentation For complete documentation, see: