feat(experimentation): add experiment exposures model, warehouse query and payload mapper#7740
feat(experimentation): add experiment exposures model, warehouse query and payload mapper#7740gagantrivedi wants to merge 12 commits into
Conversation
…nd payload mapper
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Docker builds report
|
|
/gemini review |
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-16)Details
Playwright Test Results (oss - depot-ubuntu-latest-arm-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-16)Details
Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)Details
|
There was a problem hiding this comment.
Code Review
This pull request implements the backend infrastructure for tracking and snapshotting experiment exposures from the data warehouse. It introduces the ExperimentExposureSnapshot Django model, ClickHouse queries to bucket exposures by hour or day, and mapper functions to build the exposure payload. The review feedback highlights a performance optimization opportunity in _build_timeseries_points within api/experimentation/mappers.py to reduce its time complexity from O(N^2) to O(N log N) by pre-grouping buckets.
| def _build_timeseries_points( | ||
| buckets: Sequence[ExposureBucket], | ||
| ) -> list[ExposureTimeseriesPoint]: | ||
| variant_keys = {b.variant for b in buckets} | ||
| running = dict.fromkeys(variant_keys, 0) | ||
| points: list[ExposureTimeseriesPoint] = [] | ||
| for bucket_start in sorted({b.bucket for b in buckets}): | ||
| for b in buckets: | ||
| if b.bucket == bucket_start: | ||
| running[b.variant] += b.new_units | ||
| points.append( | ||
| ExposureTimeseriesPoint( | ||
| bucket=_isoformat_utc(bucket_start), | ||
| cumulative_units=dict(running), | ||
| ) | ||
| ) | ||
| return points |
There was a problem hiding this comment.
The current implementation of _build_timeseries_points has an
We can optimize this to
def _build_timeseries_points(
buckets: Sequence[ExposureBucket],
) -> list[ExposureTimeseriesPoint]:
variant_keys = {b.variant for b in buckets}
running = dict.fromkeys(variant_keys, 0)
buckets_by_start: dict[datetime, list[ExposureBucket]] = {}
for b in buckets:
buckets_by_start.setdefault(b.bucket, []).append(b)
points: list[ExposureTimeseriesPoint] = []
for bucket_start in sorted(buckets_by_start.keys()):
for b in buckets_by_start[bucket_start]:
running[b.variant] += b.new_units
points.append(
ExposureTimeseriesPoint(
bucket=_isoformat_utc(bucket_start),
cumulative_units=dict(running),
)
)
return pointsThere was a problem hiding this comment.
Applied in dfd2b72 (both here and in _build_variants, which had the same shape). Worth noting the practical bounds, though: granularity selection caps buckets at ~72 hourly points (windows ≤72h) or ~90 daily points (90-day events TTL), and this mapper runs in the task processor when a snapshot is computed — never in the request path — so the quadratic scan couldn't reach API latency. Taking the grouped version anyway since it's no less readable and stays safe if the granularity rules ever loosen.
Visual Regression19 screenshots compared. See report for details. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7740 +/- ##
========================================
Coverage 98.54% 98.55%
========================================
Files 1452 1453 +1
Lines 55821 56001 +180
========================================
+ Hits 55011 55191 +180
Misses 810 810 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ntities Adopt Flagsmith's product vocabulary (identities) over experimentation jargon (units) across the dataclass, payload and SQL alias, and group buckets before iterating in the mapper helpers.
- compute_exposures_payload orchestrates granularity selection, query and mapping behind one entry point; granularity can no longer diverge between the query and the payload, and the selection policy moves out of the mapper. - Bucket datetimes are normalised to aware UTC at the service boundary, removing the driver workaround from the pure mapper. - ExperimentExposures gains record_refresh/record_failure so the failure-preserves-payload invariant lives on the model; last_error is dropped (structured logs cover operators, last_error_at covers the UI).
…he query Passing the timezone to the bucket function makes the result column UTC-typed, so the driver returns aware datetimes natively — and day boundaries no longer depend on the ClickHouse server timezone.
…yload Consumers receive identities per variant and the total; the share is one division away and the field couldn't name its own denominator.
…aclasses The exposures computation now constructs frozen dataclasses (ExposuresSummary and parts) instead of hand-knitting TypedDict payloads; the JSON shape is defined once, by asdict() at the model boundary. mappers.py folds into services as the pure half of the single compute_exposures_summary entry point.
…s-by-variant mapping is_control is derivable from the reserved "control" key and the control-first ordering is presentation — both move to the UI. The variants list becomes a plain mapping, symmetric with the timeseries points.
Thanks for submitting a PR! Please check the boxes below:
docs/if required so people know about the feature.Changes
Contributes to the experimentation results layer (v0.1: experiment exposures panel).
First backend slice — no tasks, API or UI yet (those follow in stacked PRs):
ExperimentExposuresmodel: one row per experiment (OneToOne), updated in place by a future task. A failed refresh recordslast_error/last_error_atwithout clobbering the last goodpayload/as_of, and there is no append-only history to prune. Each refresh recomputes the full window (started_at→as_of) — identity dedup, multi-variant quarantine and late event delivery make incremental windows unsound.get_exposure_bucketsClickHouse query: per-variant, per-time-bucket counts of first-exposed identities from$flag_exposureevents — identities count once, in the bucket of their first exposure (immune to at-least-once duplicate delivery); identities seen in more than one variant are quarantined under a$multiplesentinel; buckets are hourly for windows up to 72 hours, daily beyond.No docs changes: the feature is flag-gated and not user-visible yet; docs land with the UI.
How did you test this code?
$multipleexclusion, carry-forward cumulative series, day rounding, UTC serialisation) and the query service (bucket granularity selection, row mapping).pytest tests/unit/experimentation/— 249 passed;ruffandmypystrict clean.