Skip to content

feat(experimentation): add experiment exposures model, warehouse query and summary builder#7740

Merged
gagantrivedi merged 16 commits into
mainfrom
feat/experiment-exposure-snapshots
Jun 11, 2026
Merged

feat(experimentation): add experiment exposures model, warehouse query and summary builder#7740
gagantrivedi merged 16 commits into
mainfrom
feat/experiment-exposure-snapshots

Conversation

@gagantrivedi

@gagantrivedi gagantrivedi commented Jun 10, 2026

Copy link
Copy Markdown
Member
  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature. (deferred — flag-gated and not user-visible yet; docs land with the UI.)
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Contributes to the experimentation results layer (v0.1: experiment exposures panel). First backend slice — tasks, API and UI follow in stacked PRs.

  • ExperimentExposures — one row per experiment (OneToOne), updated in place via record_refresh(summary, as_of) / record_failure(). Failure stamps last_error_at and preserves the last good payload/as_of. Every refresh recomputes the full window (started_atas_of): identity dedup, multi-variant quarantine and late event delivery make incremental windows unsound.
  • compute_exposures_summary(...) — single entry point: selects bucket granularity (hourly ≤ 72h windows, daily beyond), queries ClickHouse, builds the summary. Query and payload cannot disagree on granularity.
  • Query — first-exposed identities per variant per time bucket from $flag_exposure events. Identities count once, in the bucket of their first exposure (immune to at-least-once duplicate delivery). Identities seen in >1 variant are flagged via an out-of-band quarantined column — a $multiple sentinel would collide with a real variant of that name. Bucket boundaries pinned to UTC in the column type, so the driver returns aware datetimes.
  • ExposuresSummary — frozen dataclass tree, serialised once via dataclasses.asdict() at the model boundary. Increments-only payload: per-bucket new_identities per variant plus the excluded_identities count (quarantined identities appear nowhere else). No derivable fields — cumulative series, per-variant totals, headline total, share %, control badge and ordering are all computed client-side, using the reserved "control" key (enforced by the multivariate serializer).

How did you test this code?

  • Unit tests: summary builder (bucket grouping, chronological ordering of sparse/unordered buckets, quarantine exclusion, empty data, granularity selection via the orchestrator), query service (row mapping, UTC bucket typing, parameters), model methods (refresh clears the error marker; failure preserves the payload; stored JSON shape pinned exactly).
  • pytest tests/unit/experimentation/ — 247 passed; ruff + mypy strict clean.

@gagantrivedi gagantrivedi requested a review from a team as a code owner June 10, 2026 09:19
@gagantrivedi gagantrivedi requested review from khvn26 and removed request for a team June 10, 2026 09:19
@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview Jun 11, 2026 8:43am
flagsmith-frontend-preview Ignored Ignored Preview Jun 11, 2026 8:43am
flagsmith-frontend-staging Ignored Ignored Preview Jun 11, 2026 8:43am

Request Review

@github-actions github-actions Bot added api Issue related to the REST API feature New feature or request labels Jun 10, 2026
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Docker builds report

Image Build Status Security report
ghcr.io/flagsmith/flagsmith-api-test:pr-7740 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-api:pr-7740 Finished ✅ Results
ghcr.io/flagsmith/flagsmith:pr-7740 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-private-cloud:pr-7740 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-e2e:pr-7740 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-frontend:pr-7740 Finished ✅ Results

@gagantrivedi

Copy link
Copy Markdown
Member Author

/gemini review

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  33 seconds
commit  f41c6e6
info  🔄 Run: #17372 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  34 seconds
commit  f41c6e6
info  🔄 Run: #17372 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  43.7 seconds
commit  f41c6e6
info  🔄 Run: #17372 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  35.3 seconds
commit  f41c6e6
info  🔄 Run: #17372 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  32.8 seconds
commit  7648791
info  🔄 Run: #17375 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  38.1 seconds
commit  7648791
info  🔄 Run: #17375 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  32.8 seconds
commit  7648791
info  🔄 Run: #17375 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  1 minute, 2 seconds
commit  7648791
info  🔄 Run: #17375 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  33.1 seconds
commit  dfd2b72
info  🔄 Run: #17383 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  43.5 seconds
commit  dfd2b72
info  🔄 Run: #17383 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  34.1 seconds
commit  dfd2b72
info  🔄 Run: #17383 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  42.9 seconds
commit  dfd2b72
info  🔄 Run: #17383 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  19 passed

Details

stats  19 tests across 15 suites
duration  1 minute, 5 seconds
commit  7f49a1f
info  🔄 Run: #17384 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  39.6 seconds
commit  7f49a1f
info  🔄 Run: #17384 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  45.3 seconds
commit  7f49a1f
info  🔄 Run: #17384 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  31.4 seconds
commit  7f49a1f
info  🔄 Run: #17384 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  34.1 seconds
commit  6d45a2a
info  🔄 Run: #17385 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  43.3 seconds
commit  6d45a2a
info  🔄 Run: #17385 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  32.4 seconds
commit  6d45a2a
info  🔄 Run: #17385 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  49 seconds
commit  6d45a2a
info  🔄 Run: #17385 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  40.8 seconds
commit  edb4f2c
info  🔄 Run: #17386 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  43.7 seconds
commit  edb4f2c
info  🔄 Run: #17386 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  41.2 seconds
commit  edb4f2c
info  🔄 Run: #17386 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  1 minute, 1 second
commit  edb4f2c
info  🔄 Run: #17386 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  40.9 seconds
commit  2645353
info  🔄 Run: #17387 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  44.4 seconds
commit  2645353
info  🔄 Run: #17387 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  32.4 seconds
commit  2645353
info  🔄 Run: #17387 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  57.1 seconds
commit  2645353
info  🔄 Run: #17387 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  19 passed

Details

stats  19 tests across 15 suites
duration  1 minute, 11 seconds
commit  a1598cb
info  🔄 Run: #17388 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  45.1 seconds
commit  a1598cb
info  🔄 Run: #17388 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  39.5 seconds
commit  a1598cb
info  🔄 Run: #17388 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  4 passed

Details

stats  4 tests across 4 suites
duration  33.1 seconds
commit  a1598cb
info  🔄 Run: #17388 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  32.9 seconds
commit  7d32960
info  🔄 Run: #17389 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  43.5 seconds
commit  7d32960
info  🔄 Run: #17389 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  34 seconds
commit  7d32960
info  🔄 Run: #17389 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  40.2 seconds
commit  7d32960
info  🔄 Run: #17389 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  40.7 seconds
commit  5e7665f
info  🔄 Run: #17390 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  45.6 seconds
commit  5e7665f
info  🔄 Run: #17390 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  40.9 seconds
commit  5e7665f
info  🔄 Run: #17390 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  39.6 seconds
commit  5e7665f
info  🔄 Run: #17390 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  40.8 seconds
commit  f63c59d
info  🔄 Run: #17391 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  44 seconds
commit  f63c59d
info  🔄 Run: #17391 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  48.6 seconds
commit  f63c59d
info  🔄 Run: #17391 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  43.3 seconds
commit  f63c59d
info  🔄 Run: #17391 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  40.6 seconds
commit  c832b05
info  🔄 Run: #17392 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  45.7 seconds
commit  c832b05
info  🔄 Run: #17392 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  32.6 seconds
commit  c832b05
info  🔄 Run: #17392 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  57 seconds
commit  c832b05
info  🔄 Run: #17392 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  40.6 seconds
commit  71cf281
info  🔄 Run: #17413 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  45.4 seconds
commit  71cf281
info  🔄 Run: #17413 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  43.1 seconds
commit  71cf281
info  🔄 Run: #17413 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  1 minute, 5 seconds
commit  71cf281
info  🔄 Run: #17413 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  33.2 seconds
commit  060aa1f
info  🔄 Run: #17414 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  43.9 seconds
commit  060aa1f
info  🔄 Run: #17414 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  58.1 seconds
commit  060aa1f
info  🔄 Run: #17414 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  47.1 seconds
commit  060aa1f
info  🔄 Run: #17414 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  33.2 seconds
commit  ef36c14
info  🔄 Run: #17415 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  45.1 seconds
commit  ef36c14
info  🔄 Run: #17415 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  2 passed

Details

stats  2 tests across 2 suites
duration  40.1 seconds
commit  ef36c14
info  🔄 Run: #17415 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  54 seconds
commit  ef36c14
info  🔄 Run: #17415 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  33.1 seconds
commit  6d336b9
info  🔄 Run: #17419 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  45.3 seconds
commit  6d336b9
info  🔄 Run: #17419 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  40.8 seconds
commit  6d336b9
info  🔄 Run: #17419 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  54.8 seconds
commit  6d336b9
info  🔄 Run: #17419 (attempt 1)

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread api/experimentation/mappers.py Outdated
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Visual Regression

19 screenshots compared. See report for details.
View full report

@gagantrivedi gagantrivedi requested a review from a team as a code owner June 10, 2026 09:27
@github-actions github-actions Bot added the docs Documentation updates label Jun 10, 2026
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request docs Documentation updates labels Jun 10, 2026
@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.55%. Comparing base (a8a7758) to head (6d336b9).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #7740    +/-   ##
========================================
  Coverage   98.54%   98.55%            
========================================
  Files        1452     1454     +2     
  Lines       55821    55989   +168     
========================================
+ Hits        55011    55179   +168     
  Misses        810      810            

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…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.
@github-actions github-actions Bot added the docs Documentation updates label Jun 10, 2026
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request docs Documentation updates labels Jun 10, 2026
@github-actions github-actions Bot added docs Documentation updates and removed feature New feature or request labels Jun 10, 2026
@github-actions github-actions Bot added docs Documentation updates feature New feature or request and removed feature New feature or request docs Documentation updates labels Jun 10, 2026
…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.
@github-actions github-actions Bot added the docs Documentation updates label Jun 10, 2026
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request docs Documentation updates labels Jun 10, 2026
…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.
@github-actions github-actions Bot added docs Documentation updates feature New feature or request and removed feature New feature or request docs Documentation updates labels Jun 10, 2026
A $multiple sentinel in the variant column collides with any real
variant of that name; quarantine now travels as its own query column
and ExposureBucket field, so no variant key is reserved.
@github-actions github-actions Bot added the docs Documentation updates label Jun 11, 2026
@github-actions github-actions Bot removed feature New feature or request docs Documentation updates labels Jun 11, 2026
Unused since the control flag moved client-side, and features.constants
already defines and enforces the reservation.

@Zaimwa9 Zaimwa9 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strong stuff, really neat architecture. I think the alias is the one to resolve and maybe the upper window exclusion in the query

Comment thread api/experimentation/services.py Outdated
Comment thread api/experimentation/models.py Outdated
Comment thread api/experimentation/services.py Outdated
Comment thread api/experimentation/models.py

@Zaimwa9 Zaimwa9 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Issue related to the REST API feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants