Skip to content

feat(nimbus): simplify collision warnings#15468

Open
jaredlockhart wants to merge 17 commits into
mainfrom
15467
Open

feat(nimbus): simplify collision warnings#15468
jaredlockhart wants to merge 17 commits into
mainfrom
15467

Conversation

@jaredlockhart
Copy link
Copy Markdown
Collaborator

@jaredlockhart jaredlockhart commented Apr 28, 2026

Because

  • Seven separate audience-overlap warnings, each rendering its own card with its own verbose text, plus a fragile substring-based dedup between two of them.
  • Real detection gaps (single-feature rollout blocking a multi-feature draft, rollout-vs-rollout on different targeting) that the narrow predicates missed.
  • Real false positives — coenrolling features, disjoint channels, and disjoint advanced-targeting configs all triggered warnings they shouldn't.

This PR

  • Replaces all seven with one collision_warnings primitive: same-polarity live recipes sharing a non-coenrolling feature with overlapping locale/country/language/channel/targeting.
  • Renders one unified card. Each colliding delivery is a row with its applicable reasons inline; self-issues fold in as additional rows.
  • Compact reason labels with ? tooltips replace the verbose warning-text constants.

Validated against last week's SDK FeatureConflict telemetry: 100% recall on top-200 pairs by client volume.

Fixes #15467

@jaredlockhart jaredlockhart changed the title feat(nimbus): warn on rollout-vs-rollout single-feature slot collisions feat(nimbus): simplify collision warnings May 5, 2026
@jaredlockhart
Copy link
Copy Markdown
Collaborator Author

Before:

image

After:

image

Because

* Audience-overlap warnings missed the case where a draft rollout
  shares a feature with a single-feature LIVE rollout that has
  different targeting — the failure mode that hit vpn-limited-rollout.
* The existing feature_has_live_multifeature_experiments check
  required the live recipe to be multi-feature, and didn't enforce
  slot-pool symmetry between experiments and rollouts.

This commit

* Renames feature_has_live_multifeature_experiments to
  feature_has_live_overlapping_deliveries and drops the
  n_feature_configs > 1 filter
* Adds is_rollout=self.is_rollout so experiments and rollouts only
  warn against same-polarity live deliveries (separate slot pools)
* Splits the warning text: experiments keep the population-dilution
  wording; rollouts get a new message explaining that the
  earliest-published rollout claims the contested slot
* Renames the existing tests to match and adds two new tests covering
  the rollout-vs-rollout collision and the experiment/rollout
  symmetry

Fixes #15467
…ings

Because

* When the warning fires, the user needs to know which feature is the
  contention point — naming the colliding delivery alone leaves them
  guessing which of the draft's features is the slot in conflict.

This commit

* Changes feature_has_live_overlapping_deliveries to return a list of
  {slug, shared_features} dicts instead of bare slugs
* Renders each entry in the warning card as `<slug> (Shares <feat1>,
  <feat2>)` when details are available, falling back to bare slug
  rendering for warnings that don't carry per-entry detail
* Updates the audience_overlap_warnings dedup helper and the affected
  tests to the new shape
Because

* The previous warning tests mocked NimbusExperiment properties
  (excluded_live_deliveries, live_experiments_in_namespace,
  feature_has_live_overlapping_deliveries) directly, so they only
  verified the audience_overlap_warnings dict-assembly wiring — never
  the underlying ORM queries that decide what's actually overlapping.
* Mocking internal model properties hides regressions in the very
  query logic the warnings exist to surface (this is how the
  rollout-vs-rollout single-feature gap survived).

This commit

* Replaces mock-based test_excluding_experiments_warning,
  test_live_feature_overlap_warning, and test_multiple_warnings with
  factory-built experiments that trigger the real property logic
* Drops test_live_rollout_feature_overlap_warning (mock) and
  test_live_experiments_bucket_warning (mock); the rollout case is
  covered by the existing factory test
  test_rollout_feature_overlap_with_live_rollout_different_targeting,
  and the namespace warning never fires in isolation due to the
  feature_overlap dedup logic — test_multiple_warnings exercises it
  in the realistic co-occurring shape
* Strengthens test_rollout_feature_overlap_with_live_rollout_different_targeting
  to assert the rollout-specific text constant
… primitive

Because

* The audience-overlap warning system grew organically: five separate
  properties (excluded_live_deliveries, live_experiments_in_namespace,
  feature_has_live_overlapping_deliveries, rollout_conflict_warning,
  pref_targeting_rollout_collision_warning) each ran their own query
  and surfaced their own warning card, with a fragile substring-based
  dedup hack.
* All five reduce to one underlying signal — a live recipe of the same
  polarity that shares a non-coenrolling feature and overlapping
  targeting — with annotations layered on top (same namespace,
  matching configuration, sets the same preference).
* The previous implementation also missed two real cases: features
  that opt into coenrollment via allowCoenrollment shouldn't trigger
  warnings, and there's no signal of how much of the eligible
  audience a draft would actually lose to colliding live deliveries.

This commit

* Adds collision_warnings as the unified primitive: one query
  returning per-delivery {slug, reasons, publish_date_relation,
  estimated_loss}, with reasons annotated for shares-feature,
  shares-namespace, matching-configuration, and sets-same-preference
* Filters out features that allow coenrollment in the experiment's
  Firefox version range
* Adds publish-date directionality for rollout-vs-rollout collisions
  (blocked_by vs would_block) so the user sees who claims the slot
* Computes a coarse estimated_loss_percent: 100% per blocking rollout,
  population_percent per colliding experiment, capped at 100%
* Removes the now-redundant individual properties (live_experiments_in_namespace,
  rollout_conflict_warning, pref_targeting_rollout_collision_warning,
  feature_has_live_overlapping_deliveries) and the substring dedup hack
* Removes the now-orphaned warning text constants
  (EXCLUDING_EXPERIMENTS_WARNING, LIVE_EXPERIMENTS_BUCKET_WARNING,
  LIVE_FEATURE_OVERLAP_WARNING, LIVE_ROLLOUT_FEATURE_OVERLAP_WARNING,
  ERROR_ROLLOUT_BUCKET_EXISTS, PREF_TARGETING_WARNING,
  ROLLOUT_BUCKET_WARNING) since the unified card composes its own text
* Updates the warning template to render one card with per-entry
  reason badges (each with a tooltip), feature links, an estimated-loss
  banner, and a "blocked by" / "would block" arrow indicator
* Replaces the 11-test cluster of mock-based property tests with
  factory-based tests that exercise collision_warnings directly and
  cover the new annotations, coenrollment skip, publish-date
  directionality, and loss estimate
… unified card

Because

* The rollout-version and multichannel warnings were being rendered as
  their own standalone cards, but conceptually they belong with the
  collision warning — all of them are issues with the draft that may
  affect enrollment.
* A single unified card is easier to scan than three separate cards
  stacked.

This commit

* Combines collision entries and self-state issues (rollout-version,
  multichannel) into a single audience_overlap_warnings card
* Adds self_issues alongside entries in the warning dict; template
  renders them as additional list items with their own tooltip
* Adapts the card header text based on what's inside (collisions
  only, self-issues only, or both)
* Adds an integration test covering the both-collisions-and-self-issues
  case
Because

* The coarse population-product loss estimate doesn't reflect what's
  happening in the wild — it ignores audience-overlap weighting and
  treats every blocked-by-rollout case as a 100% loss. A more
  trustworthy figure should come from monitoring telemetry, not from
  static config arithmetic.

This commit

* Removes estimated_loss from _slot_collisions, estimated_loss_percent
  from collision_warnings and audience_overlap_warnings, and the
  "Estimated loss" line from the warning template
* Drops the now-stale estimated_loss assertions and the
  test_collision_warnings_estimated_loss_caps_at_100_percent test
* Keeps the publish_date_relation indicator (blocked_by / would_block)
  since that's structurally correct without depending on a noisy
  numeric estimate
…iment

Because

* Per the SDK code (enrollment.rs:809-871), evolve_enrollment_recipes
  runs the same machinery for both experiments and rollouts:
  sort_experiments_by_published_date then bucket-sample, with the
  earlier-published recipe claiming the contested feature slot and the
  loser getting FeatureConflict. The previous "if self.is_rollout"
  gate on the publish-date directionality was wrong.
* Cross-polarity (experiment vs rollout sharing a feature) is a
  separate value-override mechanism in map_features_by_feature_id —
  both deliveries still enroll, the experiment's value just takes
  precedence at value-resolution time. That's not an enrollment
  refusal so it stays out of the warning system.

This commit

* Drops the is_rollout gate on publish_date_relation so blocked_by /
  would_block surfaces for experiment-vs-experiment too
* Restates the "Shares feature" tooltip to describe the SDK behavior
  uniformly (no more rollout-specific vs experiment-specific text)
* Adds a test that publish-date directionality fires for an
  experiment-vs-experiment collision
Because

* The directionality is moot: audience_overlap_warnings only fires on
  DRAFT/PREVIEW status. A live recipe by definition published before
  the draft, so the live one always claims the feature slot. The
  publish-date comparison is just a roundabout way of saying "live
  beats draft."

This commit

* Removes the publish_date_relation field from _slot_collisions,
  collision_warnings, audience_overlap_warnings, and the template
* Drops the "blocked by" / "would block" arrow indicator on each entry
  (the warning header already conveys "these live deliveries will
  conflict with this draft")
* Restates the "Shares feature" tooltip in plain terms — the live
  delivery already holds the slot
* Drops the now-dead would_block test and the experiment-publish-date
  directionality test (the publish-date branch is gone entirely)
…argeting configs

Because

* Two deliveries on different channels (e.g., release vs nightly) have
  disjoint audiences and won't collide in practice; warning on them is
  noise.
* Two deliveries with different non-empty advanced-targeting (named
  targeting_config_slug) may also have disjoint audiences. We can't
  decide JEXL contents, but the named-targeting-slug equality is a
  practical signal: same slug = audiences overlap on that axis,
  different slug = treat as disjoint.
* Firefox version range was considered and dropped — in practice
  recipes set a minimum and successors trend higher, so version ranges
  almost always overlap.

This commit

* Adds _channels_overlap and _targeting_configs_overlap helpers and
  applies them in _slot_collisions after the existing audience_overlap
  filter
* Empty channels or NO_CHANNEL on either side is treated as "matches
  all" (no constraint)
* Empty targeting_config_slug or NO_TARGETING on either side is
  treated as "matches all"
* Adds tests covering disjoint channels (desktop and non-desktop),
  empty channels, disjoint targeting configs, and NO_TARGETING
  intersecting with anything
Because

* Long-form user-facing strings (reason labels, tooltip details, card
  header text) were inlined in audience_overlap_warnings and
  collision_warnings. The rest of the warning system keeps its strings
  in nimbus_ui/constants.py — these were the outlier.

This commit

* Adds COLLISION_LABEL_*, COLLISION_DETAIL_*, COLLISION_SELF_ISSUE_*,
  and COLLISION_CARD_HEADER_* constants on NimbusUIConstants
* Replaces the inline strings in collision_warnings,
  audience_overlap_warnings, and _collision_card_header with constant
  references
* No behavior change — strings are byte-identical
Because

* The collision card currently has only one card-level "Learn more"
  link pointing at the generic audience-overlap warnings page. Each
  reason actually has more specific documentation that explains the
  particular mechanism and consequence.

This commit

* Adds COLLISION_LEARN_MORE_* URL constants on NimbusUIConstants for
  excluded, shares-feature, shares-audience, matching-configuration,
  sets-same-preference, version-below-minimum, and multichannel
* Wires learn_more_url onto each reason and self-issue dict in
  collision_warnings and audience_overlap_warnings
* Template renders a small ↗ link next to each reason's ? tooltip when
  a learn_more_url is set
* Card-level "Learn more" remains as the generic landing-page fallback
…doc links

The arrow glyph wasn't clear enough as an affordance; the codebase
already uses the literal text "Learn more" at the card level. Match
that for the per-reason and per-self-issue doc links too.
Three of the per-reason "Learn more" links pointed at tangentially
related docs rather than the warning case itself:

* Shares feature → enrollment state machine FeatureConflict (SDK
  runtime mechanism, not draft-time warning)
* Shares an audience → bucketing fundamentals (what a namespace is,
  not why sharing one matters)
* Targeting multiple channels → generic workflow page (no anchored
  section)

Drop those links rather than send users to off-topic pages. The
card-level "Learn more" still points at the generic audience-overlap
landing as the catch-all. Followup tracked in the task folder to add
proper docs and re-link.
…nd shares-audience reasons

Main had every audience-overlap warning linking to the generic
/advanced/warnings#audience-overlap landing. Three of those got
tighter anchors in this PR (excluded, sets-same-preference, rollout
version) and two ended up with no per-reason link after dropping
tangential targets:

* Shares feature
* Shares an audience

The card-level "Learn more" still routes to the generic landing, but
per-reason discoverability regressed vs main. Wire the generic
AUDIENCE_OVERLAP_WARNING URL back onto those two reasons to preserve
the link parity, with a note in the task folder to swap to tighter
anchors once experimenter-docs gets per-case sections.
Because

* collision_warnings was doing three things at once — excluded
  deliveries, slot collisions, and pref collisions — interleaved
  inside one method. The wrapper logic (build slug→entry dict, sort)
  was scattered across the body.
* The "is targeting config X overlap with Y" predicate had grown a
  redundant has/has-not ladder when a single "matches all" check
  would do.
* The collision-warning helpers were the only methods in the class
  prefixed with `_`; the convention isn't used anywhere else.

This commit

* Splits collision_warnings into excluded_collision_reasons,
  slot_collision_reasons, pref_collision_reasons — each returns
  (slug, reason) pairs. The wrapper merges them by slug.
* Rewrites targeting_configs_overlap as a single boolean expression.
* Drops the leading-underscore prefix from the new collision helpers
  to match the rest of the class.
Because

* The same `{"", NO_CHANNEL}` literal was built four times across the
  desktop and non-desktop branches.

This commit

* Hoists it to a single `matches_all` variable at the top of the
  method.
Because

* The three existing tests all exercise paths that produce a warning
  card. CI flagged the clean-draft "no collisions, no self-issues"
  branch as uncovered.

This commit

* Adds a draft with no contending live recipes and no self-issues,
  asserts audience_overlap_warnings is [].
@jaredlockhart jaredlockhart marked this pull request as ready for review May 14, 2026 20:42
Copy link
Copy Markdown
Contributor

@yashikakhurana yashikakhurana left a comment

Choose a reason for hiding this comment

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

Lets roll this over, all looks good to me, nice test coverage, thank you @jaredlockhart

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Simplify collision warnings

2 participants