Repository navigation
feat: Spec getExperimentFlag, trackEvent and trackExposureEvent - #17
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
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.
The spec no longer stands in for Flagsmith's events API.
This comment was marked as low quality.
This comment was marked as low quality.
The feature file said an SDK records the exposure, but only the negative was covered: an identity outside a running experiment produces no event. An SDK that resolves the flag and never records anything passed. The runner now runs an experiment through the Admin API: a multivariate option keyed by the variant, a rollout enrolling every identity and allocating all of them to it, and the experiment started. One variant at 100% makes which variant an identity is served deterministic.
|
I went ahead and added it, in
Scenario: An identity in a running experiment is exposed to the variant it was served
Given the environment has the following features:
| feature | enabled | value |
| banner | true | "control" |
And a running experiment on feature "banner" serving variant "treatment" with value "hello treatment"
When the application gets the experiment flag "banner" for identity "user_123" with traits:
| trait | value |
| plan | "premium" |
Then the application receives the following flags:
| feature | enabled | value |
| banner | true | "hello treatment" |
And Flagsmith has received the following events:
| event | feature | identifier | value | traits |
| $flag_exposure | banner | user_123 | "treatment" | {"plan": "premium"} |Seven scenarios pass against Two notes from building it:
The PR description is updated to match. — OpenHands agent |
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 three scenarios, because it is two behaviours in one call. Itresolves the feature for the identity and persists its traits. It records an exposure
carrying the variant the identity was served, when that identity is in a running
experiment. And it records nothing when the identity is not — the behaviour that separates
it from
getIdentityFlags, and the one the reference implementations go out of their wayto get right.
The events path
compose.ymlbrings 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.Given a running experiment on feature … serving variant … with value …creates amultivariate option keyed by the variant, a rollout enrolling every identity and
allocating all of them to that option, and starts the experiment. One variant at 100%
makes which variant an identity is served deterministic, so the exposure's value is too.
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
Flagsmith has received the following events:polls for 60s andFlagsmith has received no eventswaits 30s.warehouse-deliveryruns withBATCH_MAX_WAIT_MS: 1000to keepthat bounded. A full green run takes 80s; the negative can only produce false passes,
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
The whole stack was brought up and the spec run against
flagsmith-python-clientwith thethree steps and
Create the SDK client with events enabledadded to its steps file. Allseven scenarios pass, in 80s.
To check the assertions are not passing vacuously, the same run was repeated against
deliberately broken steps files. An SDK that sends
"mutant"instead of the tracked valuefails
A tracked event reaches Flagsmith. One that sends an exposure for an identityoutside an experiment fails
An identity outside a running experiment is not exposed. Onethat resolves the flag through
getIdentityFlags, recording nothing, failsAn identity in a running experiment is exposed to the variant it was served. Each time, only the scenariothat should fail does.
CI confirms the stack comes up and the
@corescenarios still pass against Flagsmith2.287.0. It cannot exercise the
@eventsassertions, because every SDK stillxfails theevents client; those are covered by the local run above, and will run in CI from whichever
SDK implements the step first.
Part of #13
This pull request was opened by an AI agent (OpenHands).