From 6ffbe455bf16b37ffcf4d2d6bdc049b0b0317d13 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 8 Sep 2026 09:35:25 +0200 Subject: [PATCH 1/2] test: enable local feature flag evaluation compliance --- .github/workflows/sdk-compliance.yml | 25 +- sdk_compliance_adapter/CONTRIBUTING.md | 13 +- sdk_compliance_adapter/README.md | 27 ++ sdk_compliance_adapter/adapter.py | 160 +++++++++-- sdk_compliance_adapter/docker-compose.yml | 2 +- sdk_compliance_adapter/test_adapter.py | 327 ++++++++++++++++++++++ 6 files changed, 517 insertions(+), 37 deletions(-) create mode 100644 sdk_compliance_adapter/test_adapter.py diff --git a/.github/workflows/sdk-compliance.yml b/.github/workflows/sdk-compliance.yml index 21d080595..1108654c6 100644 --- a/.github/workflows/sdk-compliance.yml +++ b/.github/workflows/sdk-compliance.yml @@ -12,20 +12,37 @@ on: - main jobs: + adapter-tests: + name: Compliance adapter protocol tests + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + - name: Install adapter and test dependencies + run: python -m pip install -e . -r sdk_compliance_adapter/requirements.txt pytest pytest-timeout pytest-asyncio + - name: Test adapter protocol + run: python -m pytest sdk_compliance_adapter/test_adapter.py --timeout=30 + compliance: name: PostHog SDK compliance tests (capture v0) - uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@03d972e49be84402c491324320b0a0f38c2ddc53 + uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@15011d6868ee73e11333860b6392d35298b92535 # 1.1.0 with: adapter-dockerfile: "sdk_compliance_adapter/Dockerfile" adapter-context: "." - test-harness-version: "0.10.0" + test-harness-version: "1.1.0" + continue-on-error: false report-name: "sdk-compliance-report-v0" compliance-v1: name: PostHog SDK compliance tests (capture v1) - uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@03d972e49be84402c491324320b0a0f38c2ddc53 + uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@15011d6868ee73e11333860b6392d35298b92535 # 1.1.0 with: adapter-dockerfile: "sdk_compliance_adapter/Dockerfile.v1" adapter-context: "." - test-harness-version: "0.10.0" + test-harness-version: "1.1.0" + continue-on-error: false report-name: "sdk-compliance-report-v1" diff --git a/sdk_compliance_adapter/CONTRIBUTING.md b/sdk_compliance_adapter/CONTRIBUTING.md index a1e0fe163..f76cab02a 100644 --- a/sdk_compliance_adapter/CONTRIBUTING.md +++ b/sdk_compliance_adapter/CONTRIBUTING.md @@ -4,7 +4,16 @@ This package contains the PostHog Python SDK compliance adapter used with the Po ## Running tests -Tests run automatically in CI via GitHub Actions. +Tests run automatically in CI via GitHub Actions against harness **1.1.0**, for both capture protocols. Both jobs opt into the local-evaluation suite and fail on compliance regressions. + +Run adapter protocol tests from the repository root in an activated virtual environment: + +```bash +python -m pip install -e . -r sdk_compliance_adapter/requirements.txt pytest pytest-timeout pytest-asyncio +python -m pytest sdk_compliance_adapter/test_adapter.py --timeout=30 +``` + +These tests exercise the real SDK loader/evaluator with controlled transports, including failed/late reloads, local false versus inconclusive, and forced remote evaluation. ### Locally with Docker Compose @@ -35,7 +44,7 @@ docker run -d --name sdk-adapter --network test-network -p 8080:8080 posthog-pyt docker run --rm \ --name test-harness \ --network test-network \ - ghcr.io/posthog/sdk-test-harness:0.10.0 \ + ghcr.io/posthog/sdk-test-harness:1.1.0 \ run --adapter-url http://sdk-adapter:8080 --mock-url http://test-harness:8081 # Cleanup diff --git a/sdk_compliance_adapter/README.md b/sdk_compliance_adapter/README.md index 5cd730c3e..a204fc59e 100644 --- a/sdk_compliance_adapter/README.md +++ b/sdk_compliance_adapter/README.md @@ -24,6 +24,8 @@ The adapter implements the standard SDK adapter interface defined in the [test h - `POST /capture` - Capture an event - `POST /flush` - Flush pending events - `GET /state` - Return internal state +- `POST /get_feature_flag` - Evaluate a flag locally or remotely +- `POST /reload_feature_flag_definitions` - Fresh, bounded definitions readiness barrier - `POST /reset` - Reset SDK state ### Key Implementation Details @@ -34,6 +36,31 @@ The adapter implements the standard SDK adapter interface defined in the [test h **UUID Tracking**: Extracts and tracks UUIDs from batches to verify deduplication. +### Local feature flag evaluation + +Both capture adapters advertise `feature_flags_local_evaluation_v1` for harness +**1.1.0**. The capability versions the adapter protocol and tests both legacy and +explicit property matching; it does not change the SDK's default matching mode. + +- `/init` maps optional `personal_api_key` to the SDK's `secret_key`. Ordinary + capture/remote tests do not need it. Background polling is disabled in the + adapter; explicit reloads still use the real SDK definitions loader. +- `/reload_feature_flag_definitions` takes `timeout_ms` (default 5000, range + 1–30000). It waits for a fresh successful publication, not merely an existing + snapshot. Failed fetches and authorization/quota resets return `ready: false`. + A timeout returns HTTP 504; the SDK's in-flight request may finish later on its + original Client, and another reload on that Client is rejected while it runs. +- `/get_feature_flag` with `only_evaluate_locally: true` uses the SDK's local-only + result API without emitting flag-called events. A conclusive false has + `locally_evaluated: true`; an inconclusive result has `value: null`, + `success: false`, and `locally_evaluated: false`, never remote fallback. +- `force_remote: true` conflicts with local-only mode. When definitions are + enabled, forced remote calls use a separate definitions-free SDK Client so + they cannot accidentally resolve from local rules. Legacy remote responses + and flag-called events are preserved. Reset disposes both Clients. + +The adapter is sequential (it does not advertise parallel-test support). + ## Documentation For complete documentation on the test harness and how to implement adapters, see: diff --git a/sdk_compliance_adapter/adapter.py b/sdk_compliance_adapter/adapter.py index b9c0de33d..0d803ae52 100644 --- a/sdk_compliance_adapter/adapter.py +++ b/sdk_compliance_adapter/adapter.py @@ -78,25 +78,26 @@ def __init__(self): self.last_error: Optional[str] = None self.requests_made: List[RequestInfo] = [] self.client: Optional[Client] = None + self.remote_client: Client | None = None + self.reload_thread: threading.Thread | None = None self.retry_attempts: Dict[str, int] = {} # Track retry attempts by batch ID def reset(self): """Reset all state""" - client_to_shutdown = None with self.lock: - client_to_shutdown = self.client + clients_to_shutdown = (self.client, self.remote_client) self.client = None - - if client_to_shutdown: - # Flush and shutdown the existing client outside state.lock. - # The patched transport records successful flush requests through - # SDKState.record_request(), which also needs state.lock. Holding the - # lock while shutdown() waits for the queue to drain can deadlock when - # a pending background event is being flushed during test reset. - try: - client_to_shutdown.shutdown() - except Exception as e: - logger.warning(f"Error shutting down client: {e}") + self.remote_client = None + # A timed-out load only owns its old Client, never a replacement. + self.reload_thread = None + + for client in clients_to_shutdown: + if client: + # Flush outside state.lock: transport instrumentation needs it. + try: + client.shutdown() + except Exception as e: + logger.warning(f"Error shutting down client: {e}") with self.lock: self.pending_events = 0 @@ -309,6 +310,7 @@ def health(): if is_v1() else ["capture_v0", "capture_ai_v0", "encoding_gzip"] ) + capabilities.append("feature_flags_local_evaluation_v1") return jsonify( { "sdk_name": "posthog-python", @@ -352,21 +354,29 @@ def init(): # One adapter process speaks one capture protocol, selected by CAPTURE_MODE. capture_mode = "v1" if is_v1() else "v0" - # Create client - client = Client( - project_api_key=api_key, - host=host, - flush_at=flush_at, - flush_interval=flush_interval, - gzip=enable_compression, - max_retries=max_retries, - debug=False, - disable_geoip=disable_geoip, - historical_migration=historical_migration, - capture_mode=capture_mode, - ) - + # Explicit reloads exercise the real loader without background polling + # racing the harness's per-test definition snapshots. + client_options = { + "project_api_key": api_key, + "host": host, + "flush_at": flush_at, + "flush_interval": flush_interval, + "gzip": enable_compression, + "max_retries": max_retries, + "debug": False, + "disable_geoip": disable_geoip, + "historical_migration": historical_migration, + "capture_mode": capture_mode, + "enable_local_evaluation": False, + } + personal_api_key = data.get("personal_api_key") + client = Client(**client_options, secret_key=personal_api_key) state.client = client + if personal_api_key: + # The SDK has no force-remote switch once definitions are loaded. + # A definitions-free Client preserves the real remote API and its + # event side effects without mutating the local Client's snapshot. + state.remote_client = Client(**client_options) logger.info( f"Initialized SDK with api_key={api_key[:10]}..., host={host}, " @@ -562,6 +572,70 @@ def get_state(): return jsonify({"error": str(e)}), 500 +@app.route("/reload_feature_flag_definitions", methods=["POST"]) +def reload_feature_flag_definitions(): + """Bound a fresh SDK load and acknowledge only its successful publication.""" + data = request.json or {} + timeout_ms = data.get("timeout_ms", 5000) + if type(timeout_ms) is not int or not 1 <= timeout_ms <= 30000: + return jsonify(success=False, ready=False, error="Invalid timeout_ms"), 400 + + errors = [] + with state.lock: + client = state.client + if client is None or not client.personal_api_key: + return jsonify( + success=False, ready=False, error="A personal_api_key is required" + ), 400 + if state.reload_thread and state.reload_thread.is_alive(): + return jsonify( + success=False, + ready=False, + error="A definitions reload is still running", + ), 409 + previous_generation = client._flag_definition_published_generation + + def load(): + try: + client.load_feature_flags() + except Exception as error: + logger.exception("Error reloading feature flag definitions") + errors.append(str(error)) + + worker = threading.Thread(target=load, daemon=True) + state.reload_thread = worker + worker.start() + + # The SDK's definitions transport timeout is longer than the adapter's + # deadline. Do not block this endpoint on it or start overlapping reloads. + worker.join(timeout_ms / 1000) + if worker.is_alive(): + return jsonify( + success=False, ready=False, error="Definitions reload timed out" + ), 504 + if errors: + return jsonify(success=False, ready=False, error=errors[0]), 502 + with state.lock: + if state.client is not client: + return jsonify( + success=False, ready=False, error="SDK reset during reload" + ), 409 + # load_feature_flags returns None even on failure. Its publication generation + # advances on successful GET/304 and auth/quota resets; the latter clear the + # fingerprint. Checking both avoids acknowledging stale or reset definitions. + with client._flag_definition_publication_lock: + ready = ( + client._flag_definition_published_generation > previous_generation + and bool(client._flag_definition_fingerprint) + and client.feature_flags is not None + ) + if not ready: + return jsonify( + success=False, ready=False, error="Fresh definitions were not loaded" + ), 502 + return jsonify(success=True, ready=True) + + @app.route("/get_feature_flag", methods=["POST"]) def get_feature_flag(): """Evaluate a feature flag""" @@ -577,14 +651,40 @@ def get_feature_flag(): groups = data.get("groups") group_properties = data.get("group_properties") disable_geoip = data.get("disable_geoip") - force_remote = data.get("force_remote", True) + only_evaluate_locally = data.get("only_evaluate_locally", False) + force_remote = data.get("force_remote", not only_evaluate_locally) + if only_evaluate_locally and force_remote: + return jsonify( + {"error": "only_evaluate_locally conflicts with force_remote"} + ), 400 if not key: return jsonify({"error": "key is required"}), 400 if not distinct_id: return jsonify({"error": "distinct_id is required"}), 400 - value = state.client.get_feature_flag( + if only_evaluate_locally: + result = state.client.get_feature_flag_result( + key, + distinct_id, + person_properties=person_properties, + groups=groups, + group_properties=group_properties, + disable_geoip=disable_geoip, + only_evaluate_locally=True, + send_feature_flag_events=False, + ) + # The real local-only SDK API returns None for inconclusive results; + # a conclusive false is a FeatureFlagResult, not a cache miss. + conclusive = result is not None + return jsonify( + success=conclusive, + value=result.get_value() if result is not None else None, + locally_evaluated=conclusive, + ) + + client = (state.remote_client or state.client) if force_remote else state.client + value = client.get_feature_flag( key, distinct_id, person_properties=person_properties, @@ -598,7 +698,7 @@ def get_feature_flag(): # the adapter action returns. Otherwise the harness may reset mock-server # state for the next test while the background consumer is still flushing, # leaking the previous test's event into the next test. - state.client.flush() + client.flush() logger.info(f"Feature flag {key} for {distinct_id}: {value}") diff --git a/sdk_compliance_adapter/docker-compose.yml b/sdk_compliance_adapter/docker-compose.yml index e6519de56..2f9ded30e 100644 --- a/sdk_compliance_adapter/docker-compose.yml +++ b/sdk_compliance_adapter/docker-compose.yml @@ -23,7 +23,7 @@ services: # Test harness test-harness: - image: ghcr.io/posthog/sdk-test-harness:0.10.0 + image: ghcr.io/posthog/sdk-test-harness:1.1.0 command: ["run", "--adapter-url", "http://sdk-adapter:8080", "--mock-url", "http://test-harness:8081"] networks: - test-network diff --git a/sdk_compliance_adapter/test_adapter.py b/sdk_compliance_adapter/test_adapter.py new file mode 100644 index 000000000..8acc78386 --- /dev/null +++ b/sdk_compliance_adapter/test_adapter.py @@ -0,0 +1,327 @@ +"""Adapter protocol tests, run separately from the SDK's optional-dependency suite.""" + +import importlib.util +import threading +import time +from pathlib import Path +from unittest.mock import Mock + +import pytest + +import posthog.capture_v1 +import posthog.client +import posthog.consumer +import posthog.request +from posthog.request import APIError, GetResponse + + +@pytest.fixture +def adapter(monkeypatch): + # Importing the adapter installs transport instrumentation. Restore it after + # every test so collecting these tests alongside SDK tests is safe. + for module, name in [ + (posthog.request, "batch_post"), + (posthog.consumer, "batch_post"), + (posthog.capture_v1, "_post_v1"), + ]: + monkeypatch.setattr(module, name, getattr(module, name)) + spec = importlib.util.spec_from_file_location( + "compliance_adapter_test", Path(__file__).with_name("adapter.py") + ) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + module.app.config["TESTING"] = True + yield module + module.state.reset() + + +def definitions(version=1): + return { + "flags": [ + { + "id": 1, + "key": "flag", + "active": True, + "filters": { + "groups": [ + { + "rollout_percentage": 100, + "properties": [ + { + "key": "plan", + "value": False, + "operator": "exact", + "type": "person", + } + ], + } + ] + }, + } + ], + "cohorts": {}, + "group_type_mapping": {}, + "property_matching_version": version, + } + + +def initialize(adapter, **overrides): + config = { + "api_key": "phc_test_key", + "host": "http://127.0.0.1:1", + "personal_api_key": "phx_test_key", + } + config.update(overrides) + response = adapter.app.test_client().post("/init", json=config) + assert response.status_code == 200 + assert response.json["success"] is True + return adapter.app.test_client() + + +@pytest.mark.parametrize("mode,capability", [("", "capture_v0"), ("v1", "capture_v1")]) +def test_health_opts_into_local_evaluation_without_losing_capture( + adapter, monkeypatch, mode, capability +): + monkeypatch.setattr(adapter, "CAPTURE_MODE", mode) + capabilities = adapter.app.test_client().get("/health").json["capabilities"] + assert "feature_flags_local_evaluation_v1" in capabilities + assert capability in capabilities + assert "capture_ai_v0" in capabilities + + +def test_init_enables_explicit_definitions_loading_without_polling(adapter): + initialize(adapter) + assert adapter.state.client.personal_api_key == "phx_test_key" + assert adapter.state.client.enable_local_evaluation is False + assert adapter.state.client.poller is None + assert adapter.state.remote_client.personal_api_key is None + + +def test_remote_only_init_does_not_require_a_privileged_key(adapter): + initialize(adapter, personal_api_key=None) + assert adapter.state.client.personal_api_key is None + assert adapter.state.remote_client is None + + +@pytest.mark.parametrize("version,expected", [(1, True), (2, False)]) +def test_reload_and_conclusive_local_result(adapter, monkeypatch, version, expected): + client = initialize(adapter) + get = Mock(return_value=GetResponse(data=definitions(version))) + monkeypatch.setattr(posthog.client, "get", get) + remote = Mock(side_effect=AssertionError("Local-only must never request /flags")) + monkeypatch.setattr(adapter.state.client, "_get_flags_decision", remote) + assert client.post("/reload_feature_flag_definitions", json={}).json == { + "success": True, + "ready": True, + } + response = client.post( + "/get_feature_flag", + json={ + "key": "flag", + "distinct_id": "user", + "person_properties": {"plan": "banana"}, + "only_evaluate_locally": True, + }, + ) + assert response.json == { + "success": True, + "value": expected, + "locally_evaluated": True, + } + get.assert_called_once() + assert get.call_args.args[0] == "phx_test_key" + assert get.call_args.args[1].startswith("/flags/definitions?token=phc_test_key") + assert adapter.state.client.poller is None + remote.assert_not_called() + + +def test_inconclusive_local_result_is_not_reported_as_false(adapter, monkeypatch): + client = initialize(adapter) + monkeypatch.setattr( + posthog.client, "get", Mock(return_value=GetResponse(data=definitions())) + ) + client.post("/reload_feature_flag_definitions", json={}) + remote = Mock(side_effect=AssertionError("Unexpected /flags fallback")) + monkeypatch.setattr(adapter.state.client, "_get_flags_decision", remote) + response = client.post( + "/get_feature_flag", + json={"key": "flag", "distinct_id": "user", "only_evaluate_locally": True}, + ) + assert response.json["success"] is False + assert response.json["value"] is None + assert response.json["locally_evaluated"] is False + remote.assert_not_called() + + +def test_force_remote_bypasses_loaded_local_definitions(adapter, monkeypatch): + client = initialize(adapter) + monkeypatch.setattr( + posthog.client, "get", Mock(return_value=GetResponse(data=definitions())) + ) + client.post("/reload_feature_flag_definitions", json={}) + remote = Mock( + return_value=posthog.client.normalize_flags_response( + {"featureFlags": {"flag": False}} + ) + ) + monkeypatch.setattr(adapter.state.remote_client, "_get_flags_decision", remote) + monkeypatch.setattr(adapter.state.remote_client, "capture", Mock()) + response = client.post( + "/get_feature_flag", + json={ + "key": "flag", + "distinct_id": "user", + "person_properties": {"plan": "banana"}, + "force_remote": True, + }, + ) + assert response.json == {"success": True, "value": False} + remote.assert_called_once() + + +def test_rejects_conflicting_evaluation_modes(adapter): + client = initialize(adapter) + response = client.post( + "/get_feature_flag", + json={ + "key": "flag", + "distinct_id": "user", + "only_evaluate_locally": True, + "force_remote": True, + }, + ) + assert response.status_code == 400 + + +@pytest.mark.parametrize("timeout", [0, -1, 30001, True, "100", None]) +def test_reload_validates_deadline(adapter, timeout): + client = initialize(adapter) + assert ( + client.post( + "/reload_feature_flag_definitions", json={"timeout_ms": timeout} + ).status_code + == 400 + ) + + +def test_reload_requires_client_and_privileged_key(adapter): + client = adapter.app.test_client() + assert client.post("/reload_feature_flag_definitions", json={}).status_code == 400 + initialize(adapter, personal_api_key=None) + assert client.post("/reload_feature_flag_definitions", json={}).status_code == 400 + + +@pytest.mark.parametrize("status", [401, 402, 500]) +def test_failed_reload_does_not_report_previous_snapshot_ready( + adapter, monkeypatch, status +): + client = initialize(adapter) + get = Mock(return_value=GetResponse(data=definitions())) + monkeypatch.setattr(posthog.client, "get", get) + assert ( + client.post("/reload_feature_flag_definitions", json={}).json["ready"] is True + ) + get.side_effect = APIError(status, "definitions unavailable") + response = client.post("/reload_feature_flag_definitions", json={}) + assert response.status_code == 502 + assert response.json["ready"] is False + assert response.json["success"] is False + assert get.call_count == 2 + + +def test_reload_is_bounded_and_does_not_overlap_requests(adapter, monkeypatch): + client = initialize(adapter) + release = threading.Event() + get = Mock( + side_effect=lambda *args, **kwargs: ( + release.wait(2), + GetResponse(data=definitions()), + )[1] + ) + monkeypatch.setattr(posthog.client, "get", get) + try: + start = time.monotonic() + response = client.post( + "/reload_feature_flag_definitions", json={"timeout_ms": 10} + ) + assert response.status_code == 504 + assert response.json["ready"] is False + assert time.monotonic() - start < 1 + assert ( + client.post("/reload_feature_flag_definitions", json={}).status_code == 409 + ) + get.assert_called_once() + finally: + release.set() + thread = getattr(adapter.state, "reload_thread", None) + if thread: + thread.join(timeout=3) + + +def test_reset_disposes_both_clients_and_clears_reload_state(adapter, monkeypatch): + client = initialize(adapter) + local = adapter.state.client + remote = adapter.state.remote_client + local_shutdown = Mock(wraps=local.shutdown) + remote_shutdown = Mock(wraps=remote.shutdown) + monkeypatch.setattr(local, "shutdown", local_shutdown) + monkeypatch.setattr(remote, "shutdown", remote_shutdown) + assert client.post("/reset").json == {"success": True} + local_shutdown.assert_called_once() + remote_shutdown.assert_called_once() + assert adapter.state.client is None + assert adapter.state.remote_client is None + assert adapter.state.reload_thread is None + + +def test_reload_refreshes_even_when_definitions_are_empty(adapter, monkeypatch): + client = initialize(adapter) + empty = definitions() + empty["flags"] = [] + get = Mock(return_value=GetResponse(data=empty)) + monkeypatch.setattr(posthog.client, "get", get) + for _ in range(2): + assert client.post("/reload_feature_flag_definitions", json={}).json == { + "success": True, + "ready": True, + } + assert get.call_count == 2 + + +def test_timed_out_reload_cannot_publish_into_replacement_client(adapter, monkeypatch): + client = initialize(adapter) + old_client = adapter.state.client + release = threading.Event() + monkeypatch.setattr( + posthog.client, + "get", + Mock( + side_effect=lambda *args, **kwargs: ( + release.wait(2), + GetResponse(data=definitions(2)), + )[1] + ), + ) + thread = None + try: + assert ( + client.post( + "/reload_feature_flag_definitions", json={"timeout_ms": 10} + ).status_code + == 504 + ) + thread = adapter.state.reload_thread + initialize(adapter) + new_client = adapter.state.client + release.set() + thread.join(timeout=3) + assert not thread.is_alive() + assert old_client.feature_flags is not None + assert new_client is not old_client + assert new_client.feature_flags is None + assert new_client.poller is None + finally: + release.set() + if thread: + thread.join(timeout=3) From 8939d3fa4de04e984ebb3e870db391809716c5f2 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Wed, 9 Sep 2026 08:53:02 +0200 Subject: [PATCH 2/2] test: upgrade SDK compliance harness to 1.1.1 --- .github/workflows/sdk-compliance.yml | 8 ++++---- sdk_compliance_adapter/CONTRIBUTING.md | 4 ++-- sdk_compliance_adapter/README.md | 2 +- sdk_compliance_adapter/docker-compose.yml | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/sdk-compliance.yml b/.github/workflows/sdk-compliance.yml index 1108654c6..89b2eb51c 100644 --- a/.github/workflows/sdk-compliance.yml +++ b/.github/workflows/sdk-compliance.yml @@ -29,20 +29,20 @@ jobs: compliance: name: PostHog SDK compliance tests (capture v0) - uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@15011d6868ee73e11333860b6392d35298b92535 # 1.1.0 + uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@1d6b197ef46758f577f470535bb3d70e37a15fa2 # 1.1.1 with: adapter-dockerfile: "sdk_compliance_adapter/Dockerfile" adapter-context: "." - test-harness-version: "1.1.0" + test-harness-version: "1.1.1" continue-on-error: false report-name: "sdk-compliance-report-v0" compliance-v1: name: PostHog SDK compliance tests (capture v1) - uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@15011d6868ee73e11333860b6392d35298b92535 # 1.1.0 + uses: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml@1d6b197ef46758f577f470535bb3d70e37a15fa2 # 1.1.1 with: adapter-dockerfile: "sdk_compliance_adapter/Dockerfile.v1" adapter-context: "." - test-harness-version: "1.1.0" + test-harness-version: "1.1.1" continue-on-error: false report-name: "sdk-compliance-report-v1" diff --git a/sdk_compliance_adapter/CONTRIBUTING.md b/sdk_compliance_adapter/CONTRIBUTING.md index f76cab02a..3033609b3 100644 --- a/sdk_compliance_adapter/CONTRIBUTING.md +++ b/sdk_compliance_adapter/CONTRIBUTING.md @@ -4,7 +4,7 @@ This package contains the PostHog Python SDK compliance adapter used with the Po ## Running tests -Tests run automatically in CI via GitHub Actions against harness **1.1.0**, for both capture protocols. Both jobs opt into the local-evaluation suite and fail on compliance regressions. +Tests run automatically in CI via GitHub Actions against harness **1.1.1**, for both capture protocols. Both jobs opt into the local-evaluation suite and fail on compliance regressions. Run adapter protocol tests from the repository root in an activated virtual environment: @@ -44,7 +44,7 @@ docker run -d --name sdk-adapter --network test-network -p 8080:8080 posthog-pyt docker run --rm \ --name test-harness \ --network test-network \ - ghcr.io/posthog/sdk-test-harness:1.1.0 \ + ghcr.io/posthog/sdk-test-harness:1.1.1 \ run --adapter-url http://sdk-adapter:8080 --mock-url http://test-harness:8081 # Cleanup diff --git a/sdk_compliance_adapter/README.md b/sdk_compliance_adapter/README.md index a204fc59e..eae25916b 100644 --- a/sdk_compliance_adapter/README.md +++ b/sdk_compliance_adapter/README.md @@ -39,7 +39,7 @@ The adapter implements the standard SDK adapter interface defined in the [test h ### Local feature flag evaluation Both capture adapters advertise `feature_flags_local_evaluation_v1` for harness -**1.1.0**. The capability versions the adapter protocol and tests both legacy and +**1.1.1**. The capability versions the adapter protocol and tests both legacy and explicit property matching; it does not change the SDK's default matching mode. - `/init` maps optional `personal_api_key` to the SDK's `secret_key`. Ordinary diff --git a/sdk_compliance_adapter/docker-compose.yml b/sdk_compliance_adapter/docker-compose.yml index 2f9ded30e..c679f5c4f 100644 --- a/sdk_compliance_adapter/docker-compose.yml +++ b/sdk_compliance_adapter/docker-compose.yml @@ -23,7 +23,7 @@ services: # Test harness test-harness: - image: ghcr.io/posthog/sdk-test-harness:1.1.0 + image: ghcr.io/posthog/sdk-test-harness:1.1.1 command: ["run", "--adapter-url", "http://sdk-adapter:8080", "--mock-url", "http://test-harness:8081"] networks: - test-network