Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/sentry/features/temporary.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,8 @@ def register_temporary_features(manager: FeatureManager) -> None:
manager.add("projects:similarity-view", ProjectFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True)
# Enable new similarity grouping model upgrade (version-agnostic rollout)
manager.add("projects:similarity-grouping-model-upgrade", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Enable next similarity grouping model rollout (for migrating EA projects from v2 to v2.1)
manager.add("projects:similarity-grouping-model-next", ProjectFeature, FeatureHandlerStrategy.FLAGPOLE, api_expose=False)
# Starfish: extract metrics from the spans
manager.add("projects:span-metrics-extraction", ProjectFeature, FeatureHandlerStrategy.INTERNAL, api_expose=True)
manager.add("projects:span-metrics-extraction-addons", ProjectFeature, FeatureHandlerStrategy.INTERNAL, api_expose=False)
Expand Down
15 changes: 7 additions & 8 deletions src/sentry/grouping/ingest/seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from sentry.seer.signed_seer_api import SeerViewerContext
from sentry.seer.similarity.config import (
get_grouping_model_version,
get_new_model_version,
should_send_to_seer_for_training,
)
from sentry.seer.similarity.similar_issues import get_similarity_data_from_seer
Expand Down Expand Up @@ -624,10 +623,10 @@ def maybe_send_seer_for_new_model_training(
variants: dict[str, BaseVariant],
) -> None:
"""
Send a training_mode=true request to Seer for the new model version if the existing
grouphash hasn't been sent to the new version yet.
Send a training_mode=true request to Seer for the project's current non-stable model
version if the existing grouphash hasn't been sent to that version yet.

This only happens for projects that have the new model rolled out. It helps
This only happens for projects on a non-stable model (via feature flags). It helps
build data for existing groups without affecting production grouping decisions.

Args:
Expand Down Expand Up @@ -688,10 +687,10 @@ def maybe_send_seer_for_new_model_training(
},
)

# Mark the grouphash as sent to the new model so we don't send duplicate requests.
# Mark the grouphash as sent to this (non-stable) model so we don't send duplicate requests.
# We update seer_latest_training_model (not seer_model) to preserve the original
# grouping decision metadata.
if gh_metadata:
new_version = get_new_model_version()
if new_version is not None:
gh_metadata.update(seer_latest_training_model=new_version.value)
gh_metadata.update(
seer_latest_training_model=get_grouping_model_version(event.project).value
)
69 changes: 22 additions & 47 deletions src/sentry/seer/similarity/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,78 +18,53 @@
# Set to None to disable rollout entirely
SEER_GROUPING_NEW_VERSION: GroupingVersion | None = GroupingVersion.V2

# Feature flag name (version-agnostic)
# Model version to migrate EA projects from new to next
SEER_GROUPING_NEXT_VERSION: GroupingVersion | None = GroupingVersion.V2_1

# Feature flag names
SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE = "projects:similarity-grouping-model-upgrade"
SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE = "projects:similarity-grouping-model-next"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

gotta say new vs next is gonna be confusing, but i am ok with, we just need to really make an effort to not let this stay longterm

Copy link
Copy Markdown
Contributor Author

@kddubey kddubey Apr 14, 2026

Choose a reason for hiding this comment

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

believe the easiest approach to clean after this process is to delete "new" from sentry and seer, and the feature.projects:similarity-grouping-model-upgrade flag in sentry-options-automator. i'll take care of it ofc



def get_grouping_model_version(project: Project) -> GroupingVersion:
"""
Get the model version to use for grouping decisions for this project.

Returns:
- Next version if rollout is enabled for this project (for migrating EA projects from new to next)
- New version if rollout is enabled for this project
- Stable version otherwise
"""
# Early return if no new version configured
if SEER_GROUPING_NEW_VERSION is None:
return SEER_GROUPING_STABLE_VERSION

# Type is narrowed to GroupingVersion here
if features.has(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, project):
if SEER_GROUPING_NEXT_VERSION is not None and features.has(
SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, project
):
return SEER_GROUPING_NEXT_VERSION

if SEER_GROUPING_NEW_VERSION is not None and features.has(
SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, project
):
return SEER_GROUPING_NEW_VERSION
return SEER_GROUPING_STABLE_VERSION


def is_new_model_rolled_out(project: Project) -> bool:
"""
Check if the new model version is rolled out for this project.

Returns False if:
- No new version is configured (rollout disabled globally)
- Feature flag is not enabled for this project
"""
if SEER_GROUPING_NEW_VERSION is None:
return False

return features.has(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, project)


def get_new_model_version() -> GroupingVersion | None:
"""
Get the new model version being rolled out, if any.
Returns None if no rollout is in progress.
"""
return SEER_GROUPING_NEW_VERSION
return SEER_GROUPING_STABLE_VERSION


def should_send_to_seer_for_training(
project: Project,
grouphash_seer_latest_training_model: str | None,
) -> bool:
"""
Check if we should send a training_mode=true request to Seer for the new model version.
Check if we should send a training_mode=true request to Seer for the
project's current model version.

This is true when:
1. A new version is being rolled out
2. The project has the rollout feature enabled
3. The grouphash hasn't already been sent to the new version

Args:
project: The project
grouphash_seer_latest_training_model: The seer_latest_training_model value
from grouphash metadata

Returns:
True if we should send a training_mode=true request
1. The project is on a non-stable model version (via feature flags)
2. The grouphash hasn't already been sent to that version
"""
new_version = get_new_model_version()
if new_version is None:
return False

if not is_new_model_rolled_out(project):
model_version = get_grouping_model_version(project)
if model_version == SEER_GROUPING_STABLE_VERSION:
return False

if grouphash_seer_latest_training_model == new_version.value:
if grouphash_seer_latest_training_model == model_version.value:
return False

return True
1 change: 1 addition & 0 deletions src/sentry/seer/similarity/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class GroupingVersion(StrEnum):

V1 = "v1"
V2 = "v2"
V2_1 = "v2.1"


class IncompleteSeerDataError(Exception):
Expand Down
6 changes: 3 additions & 3 deletions src/sentry/seer/similarity/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,13 +347,13 @@ def stacktrace_exceeds_limits(
event: Event | GroupEvent,
variants: dict[str, BaseVariant],
referrer: ReferrerOptions,
model_version: GroupingVersion | None = None,
model_version: GroupingVersion,
) -> bool:
"""
Check if a stacktrace exceeds length limits for Seer similarity analysis.

For V1, platforms that bypass length checks (to maintain consistency with backfilled data)
have all stacktraces pass through. For V2, all platforms are subject to length checks.
have all stacktraces pass through. For non-V1 models, all platforms are subject to length checks.

If we dont bypass length checks, we use a two-step approach:
1. First check raw string length - if shorter than token limit, pass immediately
Expand Down Expand Up @@ -381,7 +381,7 @@ def stacktrace_exceeds_limits(
# matching with existing data, we bypass the filter for them (their stacktraces will be truncated).
# For V2 we apply length checks to all platforms since we're re-embedding everything anyway.
if (
model_version != GroupingVersion.V2
model_version == GroupingVersion.V1
and platform in EVENT_PLATFORMS_BYPASSING_STACKTRACE_LENGTH_CHECK
):
metrics.incr(
Expand Down
29 changes: 28 additions & 1 deletion tests/sentry/grouping/seer_similarity/test_training_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
from sentry.grouping.ingest.seer import maybe_send_seer_for_new_model_training
from sentry.models.grouphash import GroupHash
from sentry.models.grouphashmetadata import GroupHashMetadata
from sentry.seer.similarity.config import SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE
from sentry.seer.similarity.config import (
SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE,
SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE,
)
from sentry.testutils.cases import TestCase
from sentry.testutils.helpers.eventprocessing import save_new_event

Expand All @@ -30,6 +33,7 @@ def test_does_nothing_when_no_rollout(self) -> None:
"""Should not send request when no new version is being rolled out"""
with (
patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None),
patch("sentry.seer.similarity.config.SEER_GROUPING_NEXT_VERSION", None),
patch(
"sentry.grouping.ingest.seer.get_similarity_data_from_seer"
) as mock_get_similarity_data,
Expand Down Expand Up @@ -212,3 +216,26 @@ def test_captures_exception_without_failing(self) -> None:
"grouphash": self.grouphash.hash,
},
)

def test_retrains_for_next_model_when_already_trained_for_new(self) -> None:
"""v2 -> v2.1 transition: grouphash trained for v2, project now on v2.1"""
with (
patch("sentry.grouping.ingest.seer.should_call_seer_for_grouping", return_value=True),
patch(
"sentry.grouping.ingest.seer.get_similarity_data_from_seer",
return_value=([], "v2.1"),
) as mock_get_similarity_data,
self.feature(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE),
):
metadata, _ = GroupHashMetadata.objects.get_or_create(grouphash=self.grouphash)
metadata.seer_latest_training_model = "v2"
metadata.save()

maybe_send_seer_for_new_model_training(self.event, self.grouphash, self.variants)

mock_get_similarity_data.assert_called_once()
call_args = mock_get_similarity_data.call_args
assert call_args[0][0]["model"].value == "v2.1"

metadata.refresh_from_db()
assert metadata.seer_latest_training_model == "v2.1"
146 changes: 69 additions & 77 deletions tests/sentry/seer/similarity/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,103 +3,95 @@
from sentry.seer.similarity.config import (
SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE,
SEER_GROUPING_NEW_VERSION,
SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE,
SEER_GROUPING_NEXT_VERSION,
SEER_GROUPING_STABLE_VERSION,
get_grouping_model_version,
get_new_model_version,
is_new_model_rolled_out,
should_send_to_seer_for_training,
)
from sentry.seer.similarity.types import GroupingVersion
from sentry.testutils.cases import TestCase


class GetGroupingModelVersionTest(TestCase):
def test_returns_stable_when_rollout_disabled(self) -> None:
"""When new model rollout is disabled, return stable version"""
with patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None):
result = get_grouping_model_version(self.project)
assert result == SEER_GROUPING_STABLE_VERSION

def test_returns_stable_when_feature_not_enabled(self) -> None:
"""When feature flag is not enabled for project, return stable version"""
result = get_grouping_model_version(self.project)
assert result == SEER_GROUPING_STABLE_VERSION

def test_returns_new_when_feature_enabled(self) -> None:
"""When feature flag is enabled for project, return new version"""
with self.feature(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE):
result = get_grouping_model_version(self.project)
assert result == SEER_GROUPING_NEW_VERSION


class IsNewModelRolledOutTest(TestCase):
def test_returns_false_when_no_new_version(self) -> None:
"""When no new version is configured, rollout is not active"""
with patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None):
result = is_new_model_rolled_out(self.project)
assert result is False

def test_returns_false_when_feature_not_enabled(self) -> None:
"""When feature flag is not enabled, rollout is not active for project"""
result = is_new_model_rolled_out(self.project)
assert result is False

def test_returns_true_when_feature_enabled(self) -> None:
"""When feature flag is enabled, rollout is active for project"""
with self.feature(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE):
result = is_new_model_rolled_out(self.project)
assert result is True


class GetNewModelVersionTest(TestCase):
def test_returns_configured_version(self) -> None:
"""Returns the configured new model version"""
result = get_new_model_version()
assert result == GroupingVersion.V2
def test_returns_stable_when_no_flags(self) -> None:
assert get_grouping_model_version(self.project) == SEER_GROUPING_STABLE_VERSION

def test_returns_none_when_disabled(self) -> None:
"""Returns None when rollout is disabled"""
with patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None):
result = get_new_model_version()
assert result is None
def test_returns_stable_when_rollout_disabled(self) -> None:
with (
patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None),
patch("sentry.seer.similarity.config.SEER_GROUPING_NEXT_VERSION", None),
):
assert get_grouping_model_version(self.project) == SEER_GROUPING_STABLE_VERSION

def test_returns_flagged_version(self) -> None:
cases = [
(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, SEER_GROUPING_NEW_VERSION),
(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, SEER_GROUPING_NEXT_VERSION),
]
for feature_flag, expected_version in cases:
with self.subTest(feature_flag=feature_flag), self.feature(feature_flag):
assert get_grouping_model_version(self.project) == expected_version

def test_next_flag_takes_priority_over_new_flag(self) -> None:
with self.feature(
[SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE]
):
assert get_grouping_model_version(self.project) == SEER_GROUPING_NEXT_VERSION

def test_falls_back_to_new_when_next_version_is_none(self) -> None:
with (
patch("sentry.seer.similarity.config.SEER_GROUPING_NEXT_VERSION", None),
self.feature(
[SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE]
),
):
assert get_grouping_model_version(self.project) == SEER_GROUPING_NEW_VERSION


class ShouldSendToSeerForTrainingTest(TestCase):
def test_returns_false_when_no_rollout(self) -> None:
"""Returns False when no new version is being rolled out"""
with patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None):
with (
patch("sentry.seer.similarity.config.SEER_GROUPING_NEW_VERSION", None),
patch("sentry.seer.similarity.config.SEER_GROUPING_NEXT_VERSION", None),
):
result = should_send_to_seer_for_training(
self.project, grouphash_seer_latest_training_model=None
)
assert result is False

def test_returns_false_when_feature_not_enabled(self) -> None:
"""Returns False when feature flag is not enabled for project"""
def test_returns_false_when_no_flags(self) -> None:
result = should_send_to_seer_for_training(
self.project, grouphash_seer_latest_training_model=None
)
assert result is False

def test_returns_true_when_never_sent(self) -> None:
"""Returns True when grouphash has never been sent to Seer"""
with self.feature(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE):
result = should_send_to_seer_for_training(
self.project, grouphash_seer_latest_training_model=None
)
assert result is True

def test_returns_true_when_sent_to_old_version(self) -> None:
"""Returns True when grouphash was sent to an older model version"""
with self.feature(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE):
result = should_send_to_seer_for_training(
self.project, grouphash_seer_latest_training_model="v1"
)
assert result is True

def test_returns_false_when_already_sent_to_new_version(self) -> None:
"""Returns False when grouphash was already sent to the new version"""
with self.feature(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE):
result = should_send_to_seer_for_training(
self.project, grouphash_seer_latest_training_model="v2"
)
assert result is False
def test_returns_true_when_training_needed(self) -> None:
cases = [
# (feature_flag, seer_latest_training_model)
(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, None),
(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, "v1"),
(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, None),
(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, "v1"),
(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, "v2"), # v2 → v2.1 transition
]
for feature_flag, training_model in cases:
with self.subTest(feature_flag=feature_flag, training_model=training_model):
with self.feature(feature_flag):
result = should_send_to_seer_for_training(
self.project, grouphash_seer_latest_training_model=training_model
)
assert result is True

def test_returns_false_when_already_sent_to_current_version(self) -> None:
cases = [
# (feature_flag, seer_latest_training_model)
(SEER_GROUPING_NEW_MODEL_ROLLOUT_FEATURE, "v2"),
(SEER_GROUPING_NEXT_MODEL_ROLLOUT_FEATURE, "v2.1"),
]
for feature_flag, training_model in cases:
with self.subTest(feature_flag=feature_flag, training_model=training_model):
with self.feature(feature_flag):
result = should_send_to_seer_for_training(
self.project, grouphash_seer_latest_training_model=training_model
)
assert result is False
Loading
Loading