Repository navigation
feat: Spec getExperimentFlag, trackEvent and trackExposureEvent - #17
gagantrivedi wants to merge 2 commits into
Conversation
Adds the `@events` capability: three feature files covering the SDK methods that power experimentation, and the pieces the runner needs to check them. SDKs send tracking and exposure events to Flagsmith's events API, which the open-source Flagsmith image does not serve, so `events-api/` stands in for it and records what an SDK sends. Events are off by default in every SDK and its URL is not derived from the Flagsmith API URL, so scenarios that need them say `Given the SDK sends events to Flagsmith` and the runner creates the client with `Create the SDK client with events enabled` instead. SDKs that have not implemented that step report it missing, as for any other new step. Every harness now recognises a step marker starting with `Create the SDK client`, rather than only that exact text. Existing steps files are unaffected. Closes #13
There was a problem hiding this comment.
We're bringing up live Flagsmith in the spec testing workflow. I expect these endpoints to not be mocked.
There was a problem hiding this comment.
Agreed, and fixed in 3342bac — events-api/ is deleted.
I had looked for an events endpoint on the Flagsmith image and concluded there wasn't one. I'd missed that the ingestion service is a separate published image, and that your docker-compose.yml wires up the whole path. compose.yml now mirrors it: kafka + kafka-init, clickhouse + clickhouse-init, flagsmith/events-ingestion-api:0.5.1 as events, and flagsmith/warehouse-delivery:0.4.0. The SDK posts to the real ingestion server and the runner reads the delivered rows out of ClickHouse.
Three things that came with it, which are worth a look:
- Pinned Flagsmith 2.276.0 → 2.287.0. 2.276.0 has neither
experimentation/warehouses/clickhouse.pynor theexperimentation_environment_keys/experimentation_delivery_connectionsdb views the ingestion server and delivery read. 2.287.0 is the first release carrying all of them plusexperimentation/clickhouse/events.sql.EXPERIMENTATION_ENABLEDandWAREHOUSE_CREDENTIALS_SECRETare set on theflagsmithservice, the latter shared withwarehouse-delivery. Given the SDK sends events to Flagsmithnow connects a warehouse to the scenario's environment viaPOST /environments/<key>/warehouse-connections/.experimentation_environment_keysonly lists keys whose environment has a connection, so without it the ingestion server rejects the batch.clickhouse-initfetchesevents.sqlfrom the raw URL pinned tov2.287.0, the way your compose does frommain. Say the word if you'd rather vendor the schema into this repo.
One honest caveat on verification: Docker does not run in the environment I work in, so I could not bring this stack up. I exercised the runner end to end against stubs shaped like the real thing — ClickHouse's HTTP interface returning JSONEachRow with traits/metadata as JSON text, an ingestion endpoint that rejects an environment with no warehouse connection — and the six scenarios pass, with deliberate mutations failing the right ones. What CI proves on this PR is that the stack comes up and the existing scenarios still pass; it cannot exercise the assertions, because every SDK still xfails Create the SDK client with events enabled. The first real run of these scenarios will be whichever SDK implements that step first, so please treat the timings (60s arrival, 30s settle) and the ClickHouse column mapping as the parts most worth a second pair of eyes.
— OpenHands agent
| An SDK resolves a feature for an identity and records the exposure, in one call, so that | ||
| an application can show a variant and have it measured. Only an identity enrolled in a | ||
| running experiment is exposed: every other evaluation is left out of the data, so that | ||
| an experiment's results count the identities it actually reached. |
There was a problem hiding this comment.
| An SDK resolves a feature for an identity and records the exposure, in one call, so that | |
| an application can show a variant and have it measured. Only an identity enrolled in a | |
| running experiment is exposed: every other evaluation is left out of the data, so that | |
| an experiment's results count the identities it actually reached. | |
| An SDK resolves a feature for an identity and records the exposure. |
There was a problem hiding this comment.
Applied in 3342bac.
I took the same cut to track_exposure_event.feature, which you didn't comment on but reads the same way, so the three @events files are consistent: An SDK records which variant of a feature an identity was shown, as a "$flag_exposure" event. Happy to revert that one if you meant the edit narrowly.
— OpenHands agent
| An SDK sends an application's tracking events to Flagsmith, so that an experiment can | ||
| measure what the identities it exposed went on to do. Values reach Flagsmith as text, | ||
| whatever type the application tracked them as. |
There was a problem hiding this comment.
| An SDK sends an application's tracking events to Flagsmith, so that an experiment can | |
| measure what the identities it exposed went on to do. Values reach Flagsmith as text, | |
| whatever type the application tracked them as. | |
| An SDK sends an application's tracking events to Flagsmith. |
There was a problem hiding this comment.
Same as https://github.com/Flagsmith/sdk-spec/pull/17/changes#r4340296883 — ideally, we'd like the harness to use the live API we offer via docker compose.
There was a problem hiding this comment.
Done in 3342bac. runner/sdk_spec/events_api.py is gone, replaced by runner/sdk_spec/warehouse.py, which queries the ClickHouse that compose.yml connects to each @events environment:
SELECT event, feature_name, identifier, value, traits, metadata
FROM events
WHERE environment_key = {environment_key:String}
ORDER BY timestamp, eventSo the runner now reads the far end of the real delivery path — ingestion → Kafka → warehouse delivery → warehouse — rather than a recording of what the SDK posted.
Two details that changed with it:
- Events are read by the environment's API key, not the server-side key the SDK authenticates with. That matches how Flagsmith reads them itself:
WarehouseConnectionViewSet.eventspassesself.kwargs["environment_api_key"]asenvironment_key. traitsandmetadataarrive as JSON text and are decoded before comparison.metadatais still matched on just the entries the table names, which now also covers the ingestion server liftingsdk_versioninto its own column.
The waits grew to match the longer path: 60s for an event to arrive, 30s before concluding one was never sent. warehouse-delivery runs with BATCH_MAX_WAIT_MS: 1000 to keep that bounded.
— OpenHands agent
Replaces the events API stand-in with the services Flagsmith ships, mirroring its own docker-compose.yml: the SDK posts to `flagsmith/events-ingestion-api`, which queues to Kafka, from where `flagsmith/warehouse-delivery` inserts into the environment's warehouse. The spec provisions a ClickHouse as that warehouse and the runner reads the delivered events out of it. `Given the SDK sends events to Flagsmith` now also connects the warehouse to the scenario's environment, which is what makes the events API accept it. Events are keyed by the environment's API key, as Flagsmith keys them. Pins Flagsmith 2.287.0, the first release shipping the ClickHouse warehouse and the db views the ingestion server reads, and turns experimentation on. Also trims the three feature descriptions to a sentence, as reviewed.
Specs the three experimentation methods from the issue, as a new
@eventscapability.What the spec now covers
Whenstepfeatures/get_experiment_flag.featurethe application gets the experiment flag "{feature}" for identity "{identifier}" with traits:getExperimentFlagfeatures/track_event.featurethe application tracks event "{event}" for identity "{identifier}" with value {value} and metadata:trackEventfeatures/track_exposure_event.featurethe application tracks an exposure of feature "{feature}" for identity "{identifier}" with value {value} and metadata:trackExposureEventgetExperimentFlaggets two scenarios: it resolves the feature for the identity andpersists its traits, and it records no exposure for an identity outside a running
experiment — the behaviour that separates it from
getIdentityFlagsand the one thereference implementations go out of their way to get right.
The events path
compose.ymlnow brings up the experimentation services alongside Flagsmith, mirroringFlagsmith's own
docker-compose.yml, so@eventsscenarios run against the real pathrather than a stand-in for it:
experimentation/warehouses/clickhouse.py, theexperimentation_environment_keysandexperimentation_delivery_connectionsdb views that ingestion and delivery read, andexperimentation/clickhouse/events.sql.EXPERIMENTATION_ENABLEDis on, andWAREHOUSE_CREDENTIALS_SECRETis shared withwarehouse-delivery.Given the SDK sends events to Flagsmithconnects a ClickHouse warehouse to thescenario's environment (
POST /environments/<key>/warehouse-connections/) and asks for aclient with events enabled. Both are needed: events are off by default in every SDK, and
the events API only accepts an environment that has a warehouse connection.
runner/sdk_spec/warehouse.py),keyed by the environment's API key — the same key Flagsmith reads them by in
WarehouseConnectionViewSet.events.Because the events client is a separate step,
Create the SDK client with events enabled,an SDK that has not implemented it reports it missing and the scenarios behave like any
other new step:
xfailin the compatibility matrix, red in SDK repositories until they addit. Every harness's marker regex was broadened from exactly
Create the SDK clientto anymarker starting with it; existing steps files are unaffected, and
harness/rustandharness/swiftalready infer "this step creates a client" from the absence of a clientparameter, so both creation steps work there unchanged.
What a reviewer should check
Kafka, and a delivery batch, so
Flagsmith has received the following events:polls for60s and
Flagsmith has received no eventswaits 30s.warehouse-deliveryruns withBATCH_MAX_WAIT_MS: 1000to keep that bounded. The negative can only produce falsepasses, never false failures.
valueis asserted as text because all three reference SDKs stringify it, andmetadatais matched on the entries the table names, because SDKs add their own and theingestion server lifts
sdk_versioninto its own column.clickhouse-initfetchesevents.sqlfrom the raw URL pinned tov2.287.0, the wayFlagsmith's compose does from
main. Vendoring the schema here instead is an easy swap.EVENTS_SDK_URLdefaults tohttp://events:8080, the compose service, rather thanbeing derived from another URL. That keeps
run-spec.ymlunchanged, so SDK repositoriespick events up by bumping their pinned ref. Running a harness on the host needs
EVENTS_SDK_URL=http://localhost:8080, which the README says.Additive for SDKs, so this is a minor version: new scenarios, a new tag, new
Whenstepsand a new client-creating step, with no existing step reworded. The pinned Flagsmith
version moves, as a spec release's pin is expected to.
Testing
Docker does not run in the environment this was written in, so the stack could not be
brought up locally. The runner was exercised end to end against stubs shaped like the real
services — ClickHouse's HTTP interface returning
JSONEachRowwithtraits/metadataasJSON text, and an ingestion endpoint that rejects an environment with no warehouse
connection. All six scenarios pass; mutating the fake SDK to send a wrong value, or to send
an exposure outside an experiment, fails the right scenario with a readable message;
removing
Create the SDK client with events enabledleaves the four@eventsscenariosxfailand the@coreones passing. The stubs are not part of this change.CI on this PR confirms the stack comes up — all eight containers healthy,
kafka-initandclickhouse-initcompleted — and that the existing scenarios still pass against Flagsmith2.287.0. It cannot exercise the
@eventsassertions, because every SDK stillxfails theevents client. The first real run of them will be whichever SDK implements that step first.
Closes #13
This pull request was opened by an AI agent (OpenHands).