Skip to content

feat(multivariate): add per-feature unique key to multivariate options#7698

Merged
gagantrivedi merged 7 commits into
mainfrom
feat/multivariate-option-key
Jun 5, 2026
Merged

feat(multivariate): add per-feature unique key to multivariate options#7698
gagantrivedi merged 7 commits into
mainfrom
feat/multivariate-option-key

Conversation

@gagantrivedi
Copy link
Copy Markdown
Member

@gagantrivedi gagantrivedi commented Jun 3, 2026

WIP / draft. First backend slice of "give every multivariate value a stable key so we know which variant a user was exposed to".

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature. (deferred — the key isn't yet surfaced to SDKs/UI; docs will land with the SDK/experimentation slices.)
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Contributes to

Adds a nullable, per-feature-unique key to MultivariateFeatureOption, giving each variant a stable, human-readable identifier (e.g. control, variant_a). This is the foundation for attributing experimentation exposure/metrics to the specific variant a user was shown.

  • Model (features/multivariate/models.py): nullable key CharField (max_length=2000, mirroring Feature.name) with a ("feature", "key") unique_together. Postgres treats NULL as distinct, so the key stays optional now while non-null keys are unique per feature — no not-null enforcement, no backfill.
  • Migration (0009): single additive migration (field + constraint), zero data migration.
  • Serializer (features/multivariate/serializers.py): exposes key. The (feature, key) unique_together makes DRF both attach a UniqueTogetherValidator and mark the field required=True — either of which would break the existing no-key create flow — so the auto-validator and required are overridden, and uniqueness is enforced manually to return a clean 400 (the DB constraint remains the final guard).

Out of scope (follow-up slices): threading key through the flag engine + environment document, the SDK flag-response variant_key, experimentation consuming key for per-variant attribution, and the frontend variation-key input/display.

How did you test this code?

Automated tests, written TDD-first (Given/When/Then, both auth types via admin_client_new):

  • Model (tests/unit/.../test_unit_multivariate_models.py): key defaults to None; duplicate non-null key on the same feature → IntegrityError; multiple null keys on the same feature allowed; same key across different features allowed.
  • API (tests/integration/.../test_integration_multivariate.py): create with key round-trips the value; duplicate key → 400 with a clear message.

Verification run locally:

  • New multivariate suite: 44 passed.
  • Regression sweep (feature serializers, SDK environment document, engine/sdk/dynamo mappers, edge identities, import/export): 412 passed, 2 skipped.
  • ruff check, ruff format --check, and mypy (strict, incl. tests): clean.

@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 3, 2026

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 4, 2026 6:27am
flagsmith-frontend-preview Ignored Ignored Preview Jun 4, 2026 6:27am
flagsmith-frontend-staging Ignored Ignored Preview Jun 4, 2026 6:27am

Request Review

@github-actions github-actions Bot added api Issue related to the REST API feature New feature or request labels Jun 3, 2026
@gagantrivedi gagantrivedi force-pushed the feat/multivariate-option-key branch from d7fb119 to c2adfb4 Compare June 3, 2026 10:38
@gagantrivedi gagantrivedi changed the base branch from feat/experiment-metrics to main June 3, 2026 10:38
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request labels Jun 3, 2026
Add a nullable, per-feature-unique `key` to MultivariateFeatureOption so
each variant carries a stable, human-readable identifier. This is the first
slice towards attributing experimentation exposure data to the specific
variant a user was shown.

- Model: nullable `key` CharField with a (feature, key) unique_together
  constraint. NULLs remain distinct in Postgres, so the key stays optional
  while non-null keys are unique per feature.
- Serializer: expose `key`, and enforce uniqueness manually to return a clean
  400 (the auto-generated UniqueTogetherValidator would otherwise force `key`
  to be required on create).
@gagantrivedi gagantrivedi force-pushed the feat/multivariate-option-key branch from c2adfb4 to df85009 Compare June 3, 2026 10:44
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request labels Jun 3, 2026
@gagantrivedi gagantrivedi marked this pull request as ready for review June 3, 2026 10:46
@gagantrivedi gagantrivedi requested a review from a team as a code owner June 3, 2026 10:46
@gagantrivedi gagantrivedi requested review from khvn26 and removed request for a team June 3, 2026 10:46
@gagantrivedi gagantrivedi changed the title feat(multivariate): add per-feature unique key to multivariate options [WIP] feat(multivariate): add per-feature unique key to multivariate options Jun 3, 2026
@gagantrivedi gagantrivedi requested a review from Zaimwa9 June 3, 2026 10:46
@gagantrivedi gagantrivedi removed the request for review from khvn26 June 3, 2026 10:46
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request labels Jun 3, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Docker builds report

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.52%. Comparing base (1946ec5) to head (0ecedaa).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7698   +/-   ##
=======================================
  Coverage   98.52%   98.52%           
=======================================
  Files        1444     1445    +1     
  Lines       54971    55061   +90     
=======================================
+ Hits        54161    54251   +90     
  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.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  34.2 seconds
commit  df85009
info  🔄 Run: #17225 (attempt 1)

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  42.7 seconds
commit  df85009
info  🔄 Run: #17225 (attempt 1)

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  1 minute, 4 seconds
commit  df85009
info  🔄 Run: #17225 (attempt 1)

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

failed  1 failed
passed  1 passed

Details

stats  2 tests across 2 suites
duration  54.2 seconds
commit  df85009
info  📦 Artifacts: View test results and HTML report
🔄 Run: #17225 (attempt 1)

Failed tests

firefox › tests/environment-permission-test.pw.ts › Environment Permission Tests › Environment-level permissions control access to features, identities, and segments @enterprise

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

passed  3 passed

Details

stats  3 tests across 3 suites
duration  34 seconds
commit  df85009
info  🔄 Run: #17225 (attempt 2)

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  34 seconds
commit  8cb001d
info  🔄 Run: #17232 (attempt 1)

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  44.7 seconds
commit  8cb001d
info  🔄 Run: #17232 (attempt 1)

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

passed  3 passed

Details

stats  3 tests across 3 suites
duration  33.8 seconds
commit  8cb001d
info  🔄 Run: #17232 (attempt 1)

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

passed  2 passed

Details

stats  2 tests across 2 suites
duration  57.4 seconds
commit  8cb001d
info  🔄 Run: #17232 (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  39206f0
info  🔄 Run: #17233 (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  39206f0
info  🔄 Run: #17233 (attempt 1)

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

passed  2 passed

Details

stats  2 tests across 2 suites
duration  32.3 seconds
commit  39206f0
info  🔄 Run: #17233 (attempt 1)

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

passed  3 passed

Details

stats  3 tests across 3 suites
duration  43.8 seconds
commit  39206f0
info  🔄 Run: #17233 (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  30c338d
info  🔄 Run: #17234 (attempt 1)

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  44.6 seconds
commit  30c338d
info  🔄 Run: #17234 (attempt 1)

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  52.9 seconds
commit  30c338d
info  🔄 Run: #17234 (attempt 1)

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  1 minute, 9 seconds
commit  30c338d
info  🔄 Run: #17234 (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  43bc44f
info  🔄 Run: #17235 (attempt 1)

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  46.1 seconds
commit  43bc44f
info  🔄 Run: #17235 (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  43bc44f
info  🔄 Run: #17235 (attempt 1)

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

passed  2 passed

Details

stats  2 tests across 2 suites
duration  39.8 seconds
commit  43bc44f
info  🔄 Run: #17235 (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  ec3411b
info  🔄 Run: #17237 (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  ec3411b
info  🔄 Run: #17237 (attempt 1)

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

passed  2 passed

Details

stats  2 tests across 2 suites
duration  33.3 seconds
commit  ec3411b
info  🔄 Run: #17237 (attempt 1)

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  41 seconds
commit  ec3411b
info  🔄 Run: #17237 (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  0ecedaa
info  🔄 Run: #17238 (attempt 1)

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

passed  1 passed

Details

stats  1 test across 1 suite
duration  39.9 seconds
commit  0ecedaa
info  🔄 Run: #17238 (attempt 1)

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

passed  3 passed

Details

stats  3 tests across 3 suites
duration  32.9 seconds
commit  0ecedaa
info  🔄 Run: #17238 (attempt 1)

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

passed  4 passed

Details

stats  4 tests across 4 suites
duration  40.8 seconds
commit  0ecedaa
info  🔄 Run: #17238 (attempt 1)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Visual Regression

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

Copy link
Copy Markdown
Contributor

@Zaimwa9 Zaimwa9 left a comment

Choose a reason for hiding this comment

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

Flagged 2 possible loopholes but otherwise I haven't identified other leaks into existing MV features.
It's mostly about reviewing serializers using the NestedMultivariateFeatureOptionSerializer and an alignment on empty string values.
It would be nice to also add tests on updating keys in MV options and then maybe add some coverage around CreateFeatureSerializer to sleep at peace?

Comment thread api/features/multivariate/models.py Outdated
Comment thread api/features/multivariate/serializers.py
Address review: 2000 characters is excessive for an analytics identifier.
Shrink `key` to 255 characters and validate it as a slug (letters, numbers,
underscores, hyphens) using Django's built-in validator, which DRF copies to
the serializer field. Also move the field's description from a code comment
to `help_text`.

The unreleased migration 0009 is updated in place.
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request labels Jun 4, 2026
Address review: an empty string key would act as a unique value of its own
under the (feature, key) constraint, allowing only one blank key per feature
with a confusing error. Clients that mean "no key" must omit the field or
send null; an explicit empty string is now rejected with a 400.

Existing clients are unaffected: the field is optional, and omitting it
stores null. The unreleased migration 0009 is updated in place.
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request labels Jun 4, 2026
…ndpoint

Address review: NestedMultivariateFeatureOptionSerializer is reused as a
writable nested serializer by CreateFeatureSerializer, which made `key`
writable through the feature endpoints without uniqueness validation. Make
`key` read-only in the nested serializer, keeping it writable only in
MultivariateFeatureOptionSerializer where its uniqueness is validated.
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request labels Jun 4, 2026
Address review: add coverage for updating keys on multivariate options —
setting a new key, keeping an option's own key without colliding with
itself, and rejecting a sibling's key with a 400.
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request labels Jun 4, 2026
@github-actions github-actions Bot added feature New feature or request and removed feature New feature or request labels Jun 4, 2026
@gagantrivedi gagantrivedi requested a review from Zaimwa9 June 4, 2026 06:26
Setting a key via PUT is already covered by the create round-trip and the
remaining update tests (own-key self-collision and sibling-duplicate).
Copy link
Copy Markdown
Contributor

@Zaimwa9 Zaimwa9 left a comment

Choose a reason for hiding this comment

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

🤞 great thanks

@gagantrivedi gagantrivedi merged commit 6fcede7 into main Jun 5, 2026
32 checks passed
@gagantrivedi gagantrivedi deleted the feat/multivariate-option-key branch June 5, 2026 07:06
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