Skip to content

Commit fe3ba93

Browse files
authored
ref(seer-grouping): v2.1 flag (#112841)
temp flag to migrate EA projects from v2 to v2.1 can be merged independently of [seer PR stack](getsentry/seer#5735) while no project is flagged in
1 parent d8b1be7 commit fe3ba93

File tree

8 files changed

+173
-153
lines changed

8 files changed

+173
-153
lines changed

src/sentry/features/temporary.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -570,6 +570,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
570570
manager.add("projects:similarity-view", ProjectFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True)
571571
# Enable new similarity grouping model upgrade (version-agnostic rollout)
572572
manager.add("projects:similarity-grouping-model-upgrade", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
573+
# Enable next similarity grouping model rollout (for migrating EA projects from v2 to v2.1)
574+
manager.add("projects:similarity-grouping-model-next", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
573575
# Starfish: extract metrics from the spans
574576
manager.add("projects:span-metrics-extraction", ProjectFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True)
575577
manager.add("projects:span-metrics-extraction-addons", ProjectFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False)

src/sentry/grouping/ingest/seer.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from sentry.seer.signed_seer_api import SeerViewerContext
2020
from sentry.seer.similarity.config import (
2121
get_grouping_model_version,
22-
get_new_model_version,
2322
should_send_to_seer_for_training,
2423
)
2524
from sentry.seer.similarity.similar_issues import get_similarity_data_from_seer
@@ -624,10 +623,10 @@ def maybe_send_seer_for_new_model_training(
624623
variants: dict[str, BaseVariant],
625624
) -> None:
626625
"""
627-
Send a training_mode=true request to Seer for the new model version if the existing
628-
grouphash hasn't been sent to the new version yet.
626+
Send a training_mode=true request to Seer for the project's current non-stable model
627+
version if the existing grouphash hasn't been sent to that version yet.
629628
630-
This only happens for projects that have the new model rolled out. It helps
629+
This only happens for projects on a non-stable model (via feature flags). It helps
631630
build data for existing groups without affecting production grouping decisions.
632631
633632
Args:
@@ -688,10 +687,10 @@ def maybe_send_seer_for_new_model_training(
688687
},
689688
)
690689

691-
# Mark the grouphash as sent to the new model so we don't send duplicate requests.
690+
# Mark the grouphash as sent to this (non-stable) model so we don't send duplicate requests.
692691
# We update seer_latest_training_model (not seer_model) to preserve the original
693692
# grouping decision metadata.
694693
if gh_metadata:
695-
new_version = get_new_model_version()
696-
if new_version is not None:
697-
gh_metadata.update(seer_latest_training_model=new_version.value)
694+
gh_metadata.update(
695+
seer_latest_training_model=get_grouping_model_version(event.project).value
696+
)

src/sentry/seer/similarity/config.py

Lines changed: 22 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,78 +18,53 @@
1818
# Set to None to disable rollout entirely
1919
SEER_GROUPING_NEW_VERSION: GroupingVersion | None = GroupingVersion.V2
2020

21-
# Feature flag name (version-agnostic)
21+
# Model version to migrate EA projects from new to next
22+
SEER_GROUPING_NEXT_VERSION: GroupingVersion | None = GroupingVersion.V2_1
23+
24+
# Feature flag names
2225
SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE = "projects:similarity-grouping-model-upgrade"
26+
SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE = "projects:similarity-grouping-model-next"
2327

2428

2529
def get_grouping_model_version(project: Project) -> GroupingVersion:
2630
"""
2731
Get the model version to use for grouping decisions for this project.
2832
2933
Returns:
34+
- Next version if rollout is enabled for this project (for migrating EA projects from new to next)
3035
- New version if rollout is enabled for this project
3136
- Stable version otherwise
3237
"""
33-
# Early return if no new version configured
34-
if SEER_GROUPING_NEW_VERSION is None:
35-
return SEER_GROUPING_STABLE_VERSION
36-
37-
# Type is narrowed to GroupingVersion here
38-
if features.has(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, project):
38+
if SEER_GROUPING_NEXT_VERSION is not None and features.has(
39+
SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, project
40+
):
41+
return SEER_GROUPING_NEXT_VERSION
42+
43+
if SEER_GROUPING_NEW_VERSION is not None and features.has(
44+
SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, project
45+
):
3946
return SEER_GROUPING_NEW_VERSION
40-
return SEER_GROUPING_STABLE_VERSION
41-
42-
43-
def is_new_model_rolled_out(project: Project) -> bool:
44-
"""
45-
Check if the new model version is rolled out for this project.
46-
47-
Returns False if:
48-
- No new version is configured (rollout disabled globally)
49-
- Feature flag is not enabled for this project
50-
"""
51-
if SEER_GROUPING_NEW_VERSION is None:
52-
return False
53-
54-
return features.has(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, project)
5547

56-
57-
def get_new_model_version() -> GroupingVersion | None:
58-
"""
59-
Get the new model version being rolled out, if any.
60-
Returns None if no rollout is in progress.
61-
"""
62-
return SEER_GROUPING_NEW_VERSION
48+
return SEER_GROUPING_STABLE_VERSION
6349

6450

6551
def should_send_to_seer_for_training(
6652
project: Project,
6753
grouphash_seer_latest_training_model: str | None,
6854
) -> bool:
6955
"""
70-
Check if we should send a training_mode=true request to Seer for the new model version.
56+
Check if we should send a training_mode=true request to Seer for the
57+
project's current model version.
7158
7259
This is true when:
73-
1. A new version is being rolled out
74-
2. The project has the rollout feature enabled
75-
3. The grouphash hasn't already been sent to the new version
76-
77-
Args:
78-
project: The project
79-
grouphash_seer_latest_training_model: The seer_latest_training_model value
80-
from grouphash metadata
81-
82-
Returns:
83-
True if we should send a training_mode=true request
60+
1. The project is on a non-stable model version (via feature flags)
61+
2. The grouphash hasn't already been sent to that version
8462
"""
85-
new_version = get_new_model_version()
86-
if new_version is None:
87-
return False
88-
89-
if not is_new_model_rolled_out(project):
63+
model_version = get_grouping_model_version(project)
64+
if model_version == SEER_GROUPING_STABLE_VERSION:
9065
return False
9166

92-
if grouphash_seer_latest_training_model == new_version.value:
67+
if grouphash_seer_latest_training_model == model_version.value:
9368
return False
9469

9570
return True

src/sentry/seer/similarity/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class GroupingVersion(StrEnum):
1414

1515
V1 = "v1"
1616
V2 = "v2"
17+
V2_1 = "v2.1"
1718

1819

1920
class IncompleteSeerDataError(Exception):

src/sentry/seer/similarity/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -347,13 +347,13 @@ def stacktrace_exceeds_limits(
347347
event: Event | GroupEvent,
348348
variants: dict[str, BaseVariant],
349349
referrer: ReferrerOptions,
350-
model_version: GroupingVersion | None = None,
350+
model_version: GroupingVersion,
351351
) -> bool:
352352
"""
353353
Check if a stacktrace exceeds length limits for Seer similarity analysis.
354354
355355
For V1, platforms that bypass length checks (to maintain consistency with backfilled data)
356-
have all stacktraces pass through. For V2, all platforms are subject to length checks.
356+
have all stacktraces pass through. For non-V1 models, all platforms are subject to length checks.
357357
358358
If we dont bypass length checks, we use a two-step approach:
359359
1. First check raw string length - if shorter than token limit, pass immediately
@@ -381,7 +381,7 @@ def stacktrace_exceeds_limits(
381381
# matching with existing data, we bypass the filter for them (their stacktraces will be truncated).
382382
# For V2 we apply length checks to all platforms since we're re-embedding everything anyway.
383383
if (
384-
model_version != GroupingVersion.V2
384+
model_version == GroupingVersion.V1
385385
and platform in EVENT_PLATFORMS_BYPASSING_STACKTRACE_LENGTH_CHECK
386386
):
387387
metrics.incr(

tests/sentry/grouping/seer_similarity/test_training_mode.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
from sentry.grouping.ingest.seer import maybe_send_seer_for_new_model_training
44
from sentry.models.grouphash import GroupHash
55
from sentry.models.grouphashmetadata import GroupHashMetadata
6-
from sentry.seer.similarity.config import SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE
6+
from sentry.seer.similarity.config import (
7+
SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE,
8+
SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE,
9+
)
710
from sentry.testutils.cases import TestCase
811
from sentry.testutils.helpers.eventprocessing import save_new_event
912

@@ -30,6 +33,7 @@ def test_does_nothing_when_no_rollout(self) -> None:
3033
"""Should not send request when no new version is being rolled out"""
3134
with (
3235
patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None),
36+
patch("sentry.seer.similarity.config.SEER_GROUPING_NEXT_VERSION", None),
3337
patch(
3438
"sentry.grouping.ingest.seer.get_similarity_data_from_seer"
3539
) as mock_get_similarity_data,
@@ -212,3 +216,26 @@ def test_captures_exception_without_failing(self) -> None:
212216
"grouphash": self.grouphash.hash,
213217
},
214218
)
219+
220+
def test_retrains_for_next_model_when_already_trained_for_new(self) -> None:
221+
"""v2 -> v2.1 transition: grouphash trained for v2, project now on v2.1"""
222+
with (
223+
patch("sentry.grouping.ingest.seer.should_call_seer_for_grouping", return_value=True),
224+
patch(
225+
"sentry.grouping.ingest.seer.get_similarity_data_from_seer",
226+
return_value=([], "v2.1"),
227+
) as mock_get_similarity_data,
228+
self.feature(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE),
229+
):
230+
metadata, _ = GroupHashMetadata.objects.get_or_create(grouphash=self.grouphash)
231+
metadata.seer_latest_training_model = "v2"
232+
metadata.save()
233+
234+
maybe_send_seer_for_new_model_training(self.event, self.grouphash, self.variants)
235+
236+
mock_get_similarity_data.assert_called_once()
237+
call_args = mock_get_similarity_data.call_args
238+
assert call_args[0][0]["model"].value == "v2.1"
239+
240+
metadata.refresh_from_db()
241+
assert metadata.seer_latest_training_model == "v2.1"

tests/sentry/seer/similarity/test_config.py

Lines changed: 69 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -3,103 +3,95 @@
33
from sentry.seer.similarity.config import (
44
SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE,
55
SEER_GROUPING_NEW_VERSION,
6+
SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE,
7+
SEER_GROUPING_NEXT_VERSION,
68
SEER_GROUPING_STABLE_VERSION,
79
get_grouping_model_version,
8-
get_new_model_version,
9-
is_new_model_rolled_out,
1010
should_send_to_seer_for_training,
1111
)
12-
from sentry.seer.similarity.types import GroupingVersion
1312
from sentry.testutils.cases import TestCase
1413

1514

1615
class GetGroupingModelVersionTest(TestCase):
17-
def test_returns_stable_when_rollout_disabled(self) -> None:
18-
"""When new model rollout is disabled, return stable version"""
19-
with patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None):
20-
result = get_grouping_model_version(self.project)
21-
assert result == SEER_GROUPING_STABLE_VERSION
22-
23-
def test_returns_stable_when_feature_not_enabled(self) -> None:
24-
"""When feature flag is not enabled for project, return stable version"""
25-
result = get_grouping_model_version(self.project)
26-
assert result == SEER_GROUPING_STABLE_VERSION
27-
28-
def test_returns_new_when_feature_enabled(self) -> None:
29-
"""When feature flag is enabled for project, return new version"""
30-
with self.feature(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE):
31-
result = get_grouping_model_version(self.project)
32-
assert result == SEER_GROUPING_NEW_VERSION
33-
34-
35-
class IsNewModelRolledOutTest(TestCase):
36-
def test_returns_false_when_no_new_version(self) -> None:
37-
"""When no new version is configured, rollout is not active"""
38-
with patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None):
39-
result = is_new_model_rolled_out(self.project)
40-
assert result is False
41-
42-
def test_returns_false_when_feature_not_enabled(self) -> None:
43-
"""When feature flag is not enabled, rollout is not active for project"""
44-
result = is_new_model_rolled_out(self.project)
45-
assert result is False
46-
47-
def test_returns_true_when_feature_enabled(self) -> None:
48-
"""When feature flag is enabled, rollout is active for project"""
49-
with self.feature(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE):
50-
result = is_new_model_rolled_out(self.project)
51-
assert result is True
52-
53-
54-
class GetNewModelVersionTest(TestCase):
55-
def test_returns_configured_version(self) -> None:
56-
"""Returns the configured new model version"""
57-
result = get_new_model_version()
58-
assert result == GroupingVersion.V2
16+
def test_returns_stable_when_no_flags(self) -> None:
17+
assert get_grouping_model_version(self.project) == SEER_GROUPING_STABLE_VERSION
5918

60-
def test_returns_none_when_disabled(self) -> None:
61-
"""Returns None when rollout is disabled"""
62-
with patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None):
63-
result = get_new_model_version()
64-
assert result is None
19+
def test_returns_stable_when_rollout_disabled(self) -> None:
20+
with (
21+
patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None),
22+
patch("sentry.seer.similarity.config.SEER_GROUPING_NEXT_VERSION", None),
23+
):
24+
assert get_grouping_model_version(self.project) == SEER_GROUPING_STABLE_VERSION
25+
26+
def test_returns_flagged_version(self) -> None:
27+
cases = [
28+
(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, SEER_GROUPING_NEW_VERSION),
29+
(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, SEER_GROUPING_NEXT_VERSION),
30+
]
31+
for feature_flag, expected_version in cases:
32+
with self.subTest(feature_flag=feature_flag), self.feature(feature_flag):
33+
assert get_grouping_model_version(self.project) == expected_version
34+
35+
def test_next_flag_takes_priority_over_new_flag(self) -> None:
36+
with self.feature(
37+
[SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE]
38+
):
39+
assert get_grouping_model_version(self.project) == SEER_GROUPING_NEXT_VERSION
40+
41+
def test_falls_back_to_new_when_next_version_is_none(self) -> None:
42+
with (
43+
patch("sentry.seer.similarity.config.SEER_GROUPING_NEXT_VERSION", None),
44+
self.feature(
45+
[SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE]
46+
),
47+
):
48+
assert get_grouping_model_version(self.project) == SEER_GROUPING_NEW_VERSION
6549

6650

6751
class ShouldSendToSeerForTrainingTest(TestCase):
6852
def test_returns_false_when_no_rollout(self) -> None:
69-
"""Returns False when no new version is being rolled out"""
70-
with patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None):
53+
with (
54+
patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None),
55+
patch("sentry.seer.similarity.config.SEER_GROUPING_NEXT_VERSION", None),
56+
):
7157
result = should_send_to_seer_for_training(
7258
self.project, grouphash_seer_latest_training_model=None
7359
)
7460
assert result is False
7561

76-
def test_returns_false_when_feature_not_enabled(self) -> None:
77-
"""Returns False when feature flag is not enabled for project"""
62+
def test_returns_false_when_no_flags(self) -> None:
7863
result = should_send_to_seer_for_training(
7964
self.project, grouphash_seer_latest_training_model=None
8065
)
8166
assert result is False
8267

83-
def test_returns_true_when_never_sent(self) -> None:
84-
"""Returns True when grouphash has never been sent to Seer"""
85-
with self.feature(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE):
86-
result = should_send_to_seer_for_training(
87-
self.project, grouphash_seer_latest_training_model=None
88-
)
89-
assert result is True
90-
91-
def test_returns_true_when_sent_to_old_version(self) -> None:
92-
"""Returns True when grouphash was sent to an older model version"""
93-
with self.feature(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE):
94-
result = should_send_to_seer_for_training(
95-
self.project, grouphash_seer_latest_training_model="v1"
96-
)
97-
assert result is True
98-
99-
def test_returns_false_when_already_sent_to_new_version(self) -> None:
100-
"""Returns False when grouphash was already sent to the new version"""
101-
with self.feature(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE):
102-
result = should_send_to_seer_for_training(
103-
self.project, grouphash_seer_latest_training_model="v2"
104-
)
105-
assert result is False
68+
def test_returns_true_when_training_needed(self) -> None:
69+
cases = [
70+
# (feature_flag, seer_latest_training_model)
71+
(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, None),
72+
(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, "v1"),
73+
(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, None),
74+
(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, "v1"),
75+
(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, "v2"), # v2 → v2.1 transition
76+
]
77+
for feature_flag, training_model in cases:
78+
with self.subTest(feature_flag=feature_flag, training_model=training_model):
79+
with self.feature(feature_flag):
80+
result = should_send_to_seer_for_training(
81+
self.project, grouphash_seer_latest_training_model=training_model
82+
)
83+
assert result is True
84+
85+
def test_returns_false_when_already_sent_to_current_version(self) -> None:
86+
cases = [
87+
# (feature_flag, seer_latest_training_model)
88+
(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, "v2"),
89+
(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, "v2.1"),
90+
]
91+
for feature_flag, training_model in cases:
92+
with self.subTest(feature_flag=feature_flag, training_model=training_model):
93+
with self.feature(feature_flag):
94+
result = should_send_to_seer_for_training(
95+
self.project, grouphash_seer_latest_training_model=training_model
96+
)
97+
assert result is False

0 commit comments

Comments
 (0)