Repository navigation
Parked: fix(experimentation): test SRM against the rollout override's split #8798
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -355,14 +355,19 @@ def _experiment_metric_specs(experiment: "Experiment") -> list[MetricSpec]: | |
|
|
||
| def _expected_variant_shares(experiment: "Experiment") -> dict[str, float]: | ||
| """The traffic split SRM tests against: each multivariate option's | ||
| environment allocation, with ``control`` taking the unallocated remainder. | ||
| Empty when the feature has no usable allocations, skipping the SRM check.""" | ||
| # TODO: read the split from the percentage-split segment override feature | ||
| # state once that's implemented, rather than the environment default. | ||
| allocation on the rollout override, with ``control`` taking the unallocated | ||
| remainder. Only the rollout cohort records exposures, so the environment | ||
| default (zeroed once the rollout exists) doesn't count. Empty when there is | ||
| no usable split, skipping the SRM check.""" | ||
| if experiment.rollout_segment_id is None: | ||
| return {} | ||
| feature_state = ( | ||
| FeatureState.objects.get_live_feature_states( | ||
| environment=experiment.environment, | ||
| additional_filters=Q(feature_segment__isnull=True, identity__isnull=True), | ||
| additional_filters=Q( | ||
| feature_segment__segment_id=experiment.rollout_segment_id, | ||
| identity__isnull=True, | ||
| ), | ||
| feature_id=experiment.feature_id, | ||
| ) | ||
| .prefetch_related( | ||
|
|
@@ -388,8 +393,10 @@ def _expected_variant_shares(experiment: "Experiment") -> dict[str, float]: | |
| feature__id=experiment.feature_id, | ||
| ) | ||
| return {} | ||
| shares[key] = mv_value.percentage_allocation / 100 | ||
| allocated += mv_value.percentage_allocation | ||
| if mv_value.percentage_allocation > 0: | ||
| # A zero share has no expected traffic to test against. | ||
| shares[key] = mv_value.percentage_allocation / 100 | ||
|
Comment on lines
+397
to
+399
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: rg -n 'expected_shares|srm_p_value' api/experimentation/services.py
git diff b6d69889652df199a21dfa9f64a2c8a7550936f0 bb65e7e3e3d7e565e70dcf30058e3e5cffd3647d -- api/experimentation/services.pyRepository: Flagsmith/flagsmith Length of output: 3006 🏁 Script executed: set -eu
printf '%s\n' '--- current services.py relevant block ---'
nl -ba api/experimentation/services.py | sed -n '250,420p'
printf '%s\n' '--- merge-base services.py relevant block ---'
git show b6d69889652df199a21dfa9f64a2c8a7550936f0:api/experimentation/services.py | nl -ba | sed -n '250,420p'
printf '%s\n' '--- srm_p_value declaration and implementation references ---'
rg -n -F --glob '*.py' -e 'def srm_p_value' -e 'srm_p_value(' .
printf '%s\n' '--- exposure_counts construction and relevant tests ---'
rg -n -F --glob '*.py' -e 'exposure_counts' -e 'expected_shares' -e 'Sample ratio mismatch' tests apiRepository: Flagsmith/flagsmith Length of output: 21276 🏁 Script executed: set -eu
printf '%s\n' '--- srm_p_value implementation ---'
nl -ba api/experimentation/stats.py | sed -n '80,115p'
printf '%s\n' '--- focused build_results_summary tests ---'
nl -ba api/tests/unit/experimentation/test_services.py | sed -n '1160,1355p'
printf '%s\n' '--- exposure aggregation implementation ---'
nl -ba api/experimentation/results_query.py | sed -n '285,325p'Repository: Flagsmith/flagsmith Length of output: 11975 Flag exposures for variants outside When a zero-weight variant has exposures, Do not restore a zero share directly. Suggested fix if expected_shares and total >= SRM_MIN_TOTAL_IDENTITIES:
- srm = srm_p_value(
- [exposure_counts.get(variant, 0) for variant in expected_shares],
- list(expected_shares.values()),
- )
+ has_unexpected_exposures = any(
+ count > 0
+ for variant, count in exposure_counts.items()
+ if variant not in expected_shares
+ )
+ if has_unexpected_exposures:
+ srm = 0.0
+ else:
+ srm = srm_p_value(
+ [exposure_counts.get(variant, 0) for variant in expected_shares],
+ list(expected_shares.values()),
+ ) |
||
| if not shares: | ||
| return {} | ||
| if allocated > 100: | ||
|
|
@@ -402,7 +409,8 @@ def _expected_variant_shares(experiment: "Experiment") -> dict[str, float]: | |
| feature__id=experiment.feature_id, | ||
| ) | ||
| return {} | ||
| shares[CONTROL_VARIANT_KEY] = (100 - allocated) / 100 | ||
| if allocated < 100: | ||
| shares[CONTROL_VARIANT_KEY] = (100 - allocated) / 100 | ||
| return shares | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Account for split changes within the results window.
If a running experiment changes its variant weights, this query returns only the current split.
build_results_summarycompares that split with exposures aggregated across the results window. Exposures assigned under the earlier split can therefore produce a false SRM alert. Use allocation history for the exposure window, or restrict the check to exposures collected under one stable split.