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.
(experiment: "Experiment")
| 349 | |
| 350 | |
| 351 | def _expected_variant_shares(experiment: "Experiment") -> dict[str, float]: |
| 352 | """The traffic split SRM tests against: each multivariate option's |
| 353 | environment allocation, with ``control`` taking the unallocated remainder. |
| 354 | Empty when the feature has no usable allocations, skipping the SRM check.""" |
| 355 | # TODO: read the split from the percentage-split segment override feature |
| 356 | # state once that's implemented, rather than the environment default. |
| 357 | feature_state = ( |
| 358 | FeatureState.objects.get_live_feature_states( |
| 359 | environment=experiment.environment, |
| 360 | additional_filters=Q(feature_segment__isnull=True, identity__isnull=True), |
| 361 | feature_id=experiment.feature_id, |
| 362 | ) |
| 363 | .prefetch_related( |
| 364 | "multivariate_feature_state_values__multivariate_feature_option" |
| 365 | ) |
| 366 | # Highest id is the current version, matching how Environment selects |
| 367 | # active feature states (Max("id")); the default ordering is ascending. |
| 368 | .order_by("-id") |
| 369 | .first() |
| 370 | ) |
| 371 | if feature_state is None: |
| 372 | return {} |
| 373 | |
| 374 | shares: dict[str, float] = {} |
| 375 | allocated = 0.0 |
| 376 | for mv_value in feature_state.multivariate_feature_state_values.all(): |
| 377 | key = mv_value.multivariate_feature_option.key |
| 378 | if key is None: |
| 379 | # An unkeyed option's traffic can't be attributed to a variant; |
| 380 | # counting it as control would inflate control's expected share and |
| 381 | # raise a false SRM alarm, so skip the check entirely. |
| 382 | logger.error( |
| 383 | "srm.unkeyed_variant", |
| 384 | experiment__id=experiment.id, |
| 385 | environment__id=experiment.environment_id, |
| 386 | feature__id=experiment.feature_id, |
| 387 | ) |
| 388 | return {} |
| 389 | shares[key] = mv_value.percentage_allocation / 100 |
| 390 | allocated += mv_value.percentage_allocation |
| 391 | if not shares: |
| 392 | return {} |
| 393 | if allocated > 100: |
| 394 | # A misconfigured feature whose options over-allocate; control's share |
| 395 | # would be negative, so there's no valid split to test against. |
| 396 | logger.error( |
| 397 | "srm.overallocated", |
| 398 | experiment__id=experiment.id, |
| 399 | environment__id=experiment.environment_id, |
| 400 | feature__id=experiment.feature_id, |
| 401 | ) |
| 402 | return {} |
| 403 | shares[CONTROL_VARIANT_KEY] = (100 - allocated) / 100 |
| 404 | return shares |
| 405 | |
| 406 | |
| 407 | def _metric_inference( |
no test coverage detected
searching dependent graphs…