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
4 changes: 2 additions & 2 deletions src/sentry/receivers/seer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sentry.models.project import Project
from sentry.seer.autofix.constants import SeerAutomationSource
from sentry.seer.autofix.trigger import is_issue_eligible_for_seer_automation
from sentry.seer.autofix.utils import is_seer_seat_based_tier_enabled
from sentry.seer.autofix.utils import is_seer_seat_or_token_billed_enabled
from sentry.signals import integration_added, issue_assigned
from sentry.tasks.seer.autofix import summarize_issue
from sentry.tasks.seer.investigation import mark_investigation_telemetry_connected
Expand All @@ -32,7 +32,7 @@ def dispatch_first_assignment_summary(
):
return

if is_seer_seat_based_tier_enabled(project.organization):
if is_seer_seat_or_token_billed_enabled(project.organization):
logger.info(
"seer.issue_summary.first_assignment.skipped_seat_based_seer",
extra={
Expand Down
6 changes: 3 additions & 3 deletions src/sentry/seer/agentic_triage/delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
AutofixStoppingPoint,
bulk_read_preferences_from_sentry_db,
is_seer_autotriggered_autofix_rate_limited_and_increment,
is_seer_seat_based_tier_enabled,
is_seer_seat_or_token_billed_enabled,
)
from sentry.seer.models.agentic_triage import (
SeerAgenticTriageRunErrorType,
Expand Down Expand Up @@ -313,8 +313,8 @@ def _process_verdicts(

referrer = referrer_map[SeerAutomationSource.AGENTIC_TRIAGE]

# Rate limit only applies to legacy org plans
check_rate_limit = not is_seer_seat_based_tier_enabled(organization)
# Rate limit only applies to legacy usage-based orgs
check_rate_limit = not is_seer_seat_or_token_billed_enabled(organization)

for group in fixable_groups:
if check_rate_limit and is_seer_autotriggered_autofix_rate_limited_and_increment(
Expand Down
4 changes: 2 additions & 2 deletions src/sentry/seer/autofix/issue_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
AutofixStoppingPoint,
is_seer_autotriggered_autofix_rate_limited,
is_seer_autotriggered_autofix_rate_limited_and_increment,
is_seer_seat_based_tier_enabled,
is_seer_seat_or_token_billed_enabled,
)
from sentry.seer.entrypoints.cache import SeerOperatorAutofixCache
from sentry.seer.entrypoints.operator import SeerAutofixOperator
Expand Down Expand Up @@ -481,7 +481,7 @@ def get_automation_stopping_point(group: Group) -> AutofixStoppingPoint | None:
"""
user_preference = group.project.get_option("sentry:seer_automated_run_stopping_point")

if is_seer_seat_based_tier_enabled(group.organization):
if is_seer_seat_or_token_billed_enabled(group.organization):
fixability_score = get_and_update_group_fixability_score(group)
fixability_stopping_point = _get_stopping_point_from_fixability(fixability_score)

Expand Down
20 changes: 15 additions & 5 deletions src/sentry/seer/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def get_valid_automated_run_stopping_points(
organization: Organization,
) -> frozenset[AutofixStoppingPoint]:
"""Return the set of stopping points valid for an org's billing tier."""
if is_seer_seat_based_tier_enabled(organization):
if is_seer_seat_or_token_billed_enabled(organization):
return SEAT_BASED_STOPPING_POINTS
return USAGE_BASED_STOPPING_POINTS

Expand Down Expand Up @@ -1046,6 +1046,16 @@ def is_seer_seat_based_tier_enabled(organization: Organization) -> bool:
return has_seat_based_seer


def is_seer_seat_or_token_billed_enabled(organization: Organization) -> bool:
"""
Check if the organization is on seat-based or token-billed Seer, as opposed to
legacy usage-based Seer. Both use agentic triage for automation.
"""
return is_seer_seat_based_tier_enabled(organization) or features.has(
"organizations:token-billed-seer-enabled", organization
)


def is_seer_token_based_billing_onboarding_enabled(organization: Organization) -> bool:
"""Check if Seer onboarding changes for token-based billing apply to this organization."""
return features.has(
Expand All @@ -1058,17 +1068,17 @@ def is_free_cohort_org(organization: Organization) -> bool:
orgs that receive agentic triage and autofix without a Seer subscription.

Returns True when the kill switch is NOT engaged (flag disabled = cohort
active), the org is NOT on a paid seat-based Seer plan, and the org
option is set to True.
active), the org is NOT on a paid seat-based or token-billed Seer plan, and
the org option is set to True.
"""
try:
if features.has("organizations:agentic-triage-free-cohort-killswitch", organization):
return False
except Exception:
return False
return not is_seer_seat_based_tier_enabled(organization) and bool(
return bool(
OrganizationOption.objects.get_value(organization, "agentic-triage-free-cohort", False)
)
) and not is_seer_seat_or_token_billed_enabled(organization)


def is_issue_category_eligible(group: Group) -> bool:
Expand Down
8 changes: 4 additions & 4 deletions src/sentry/seer/endpoints/project_seer_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
get_token_based_billing_project_repos,
get_token_based_billing_repo_filter,
get_valid_automated_run_stopping_points,
is_seer_seat_based_tier_enabled,
is_seer_seat_or_token_billed_enabled,
is_seer_token_based_billing_onboarding_enabled,
update_seer_project_settings,
)
Expand Down Expand Up @@ -410,7 +410,7 @@ def validate(self, data: dict[str, Any]) -> dict[str, Any]:


class ProjectSettingsUpdateSerializer(_BaseProjectSettingsUpdateSerializer):
"""Seat-based (new) Seer: restricted tuning choices, stopping point sync."""
"""Seat-based and token-billed (new) Seer: restricted tuning choices, stopping point sync."""

automation_tuning = serializers.ChoiceField(
choices=[AutofixAutomationTuningSettings.OFF, AutofixAutomationTuningSettings.MEDIUM],
Expand Down Expand Up @@ -451,7 +451,7 @@ def get(self, request: Request, project: Project) -> Response:
def put(self, request: Request, project: Project) -> Response:
serializer_cls = (
ProjectSettingsUpdateSerializer
if is_seer_seat_based_tier_enabled(project.organization)
if is_seer_seat_or_token_billed_enabled(project.organization)
else LegacyProjectSettingsUpdateSerializer
)
serializer = serializer_cls(
Expand Down Expand Up @@ -521,7 +521,7 @@ def get(self, request: Request, organization: Organization) -> Response:
def put(self, request: Request, organization: Organization) -> Response:
serializer_cls = (
BulkProjectSettingsUpdateSerializer
if is_seer_seat_based_tier_enabled(organization)
if is_seer_seat_or_token_billed_enabled(organization)
else LegacyBulkProjectSettingsUpdateSerializer
)
serializer = serializer_cls(data=request.data, context={"organization": organization})
Expand Down
16 changes: 8 additions & 8 deletions src/sentry/seer/similarity/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
AutomationCodingAgent,
SeerProjectSettingsUpdate,
get_org_default_seer_automation_handoff,
is_seer_seat_based_tier_enabled,
is_seer_seat_or_token_billed_enabled,
update_seer_project_settings,
)
from sentry.seer.similarity.types import GroupingVersion
Expand Down Expand Up @@ -544,26 +544,26 @@ def set_default_project_autofix_automation_tuning(
project.update_option(
"sentry:autofix_automation_tuning", AutofixAutomationTuningSettings.OFF
)
elif is_seer_seat_based_tier_enabled(organization):
# Feature flag ON overrides everything except explicit "off"
elif is_seer_seat_or_token_billed_enabled(organization):
# Seat-based and token-billed orgs override everything except explicit "off"
project.update_option(
"sentry:autofix_automation_tuning",
AutofixAutomationTuningSettings.MEDIUM,
)
elif org_default:
# Feature flag OFF, use org's explicit value
# Legacy orgs use org's explicit value
project.update_option("sentry:autofix_automation_tuning", org_default)


def set_default_project_seer_scanner_automation(
organization: Organization, project: Project
) -> None:
"""Called once at project creation time to set the initial seer scanner automation."""
if is_seer_seat_based_tier_enabled(organization):
# Feature flag ON always sets scanner to True
if is_seer_seat_or_token_billed_enabled(organization):
# Seat-based and token-billed orgs always set scanner to True
project.update_option("sentry:seer_scanner_automation", True)
else:
# Feature flag OFF, use org's explicit value if set
# Legacy orgs use org's explicit value if set
org_default = organization.get_option("sentry:default_seer_scanner_automation")
if org_default is not None:
project.update_option("sentry:seer_scanner_automation", org_default)
Expand All @@ -573,7 +573,7 @@ def set_default_project_seer_preferences(organization: Organization, project: Pr
"""Called once at project creation time to set the initial automated run stopping
point and automation handoff.
"""
if not is_seer_seat_based_tier_enabled(organization):
if not is_seer_seat_or_token_billed_enabled(organization):
return

stopping_point, automation_handoff = get_org_default_seer_automation_handoff(organization)
Expand Down
6 changes: 3 additions & 3 deletions src/sentry/tasks/post_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -1643,7 +1643,7 @@ def kick_off_seer_automation(job: PostProcessJob) -> None:
get_issue_summary_lock_key,
)
from sentry.seer.autofix.trigger import get_default_seer_automation_skip_reason
from sentry.seer.autofix.utils import is_seer_seat_based_tier_enabled
from sentry.seer.autofix.utils import is_seer_seat_or_token_billed_enabled
from sentry.tasks.seer.autofix import (
generate_issue_summary_only,
generate_summary_and_run_automation,
Expand All @@ -1652,15 +1652,15 @@ def kick_off_seer_automation(job: PostProcessJob) -> None:
event = job["event"]
group = event.group

if is_seer_seat_based_tier_enabled(group.organization):
if is_seer_seat_or_token_billed_enabled(group.organization):
if group.seer_fixability_score is not None:
return
# Issues created in last 5 minutes only. This can be removed once this is live past 1 week.
# We don't want to backfill old issues since that will overwhelm Seer.
if (timezone.now() - group.first_seen).total_seconds() > 300:
return
# Generate issue summary and fixability score if not cached.
# Agentic Triage handles automations for seat-based orgs but still needs these.
# Agentic Triage handles automations for seat-based and token-billed orgs but still needs these.
Comment thread
sentry-junior[bot] marked this conversation as resolved.
Resolved
cache_key = get_issue_summary_cache_key(group.id)
if cache.get(cache_key) is None:
lock_key, lock_name = get_issue_summary_lock_key(group.id)
Expand Down
6 changes: 3 additions & 3 deletions src/sentry/tasks/seer/agentic_triage/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
bulk_read_preferences_from_sentry_db,
is_free_cohort_org,
is_seer_autotriggered_autofix_rate_limited,
is_seer_seat_based_tier_enabled,
is_seer_seat_or_token_billed_enabled,
is_seer_token_based_billing_onboarding_enabled,
)
from sentry.seer.models import SeerPermissionError
Expand Down Expand Up @@ -704,7 +704,7 @@ class EligibleProject:
tweaks: AgenticTriageTweaks
stopping_point: AutofixStoppingPoint
connected_repos: list[str]
# None for seat-based orgs, regardless of the project's own setting.
# None for seat-based and token-billed orgs, regardless of the project's own setting.
automation_tuning: AutofixAutomationTuningSettings | None


Expand Down Expand Up @@ -735,7 +735,7 @@ def _get_eligible_projects(
organization.id, list(project_map), limit_repos=True
)

is_legacy_org = not is_seer_seat_based_tier_enabled(organization)
is_legacy_org = not is_seer_seat_or_token_billed_enabled(organization)

eligible: list[EligibleProject] = []
for pid, project in project_map.items():
Expand Down
6 changes: 3 additions & 3 deletions tests/sentry/core/endpoints/test_team_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ def test_project_autofix_tuning_not_set_if_org_option_not_set_in_db(self) -> Non
project=project, key="sentry:autofix_automation_tuning"
).exists()

@patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True)
@patch("sentry.seer.similarity.utils.is_seer_seat_or_token_billed_enabled", return_value=True)
def test_project_autofix_tuning_defaults_to_medium_with_seat_based_tier(
self, mock_seat_based_tier: MagicMock
) -> None:
Expand All @@ -665,7 +665,7 @@ def test_project_autofix_tuning_defaults_to_medium_with_seat_based_tier(
)
assert autofix_tuning == "medium"

@patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True)
@patch("sentry.seer.similarity.utils.is_seer_seat_or_token_billed_enabled", return_value=True)
def test_project_autofix_tuning_respects_explicit_off_even_with_seat_based_tier(
self, mock_seat_based_tier: MagicMock
) -> None:
Expand All @@ -685,7 +685,7 @@ def test_project_autofix_tuning_respects_explicit_off_even_with_seat_based_tier(
)
assert autofix_tuning == "off"

@patch("sentry.seer.similarity.utils.is_seer_seat_based_tier_enabled", return_value=True)
@patch("sentry.seer.similarity.utils.is_seer_seat_or_token_billed_enabled", return_value=True)
def test_project_autofix_tuning_seat_based_tier_overrides_non_off_org_default(
self, mock_seat_based_tier: MagicMock
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/sentry/seer/agentic_triage/test_delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,7 @@ def test_seat_based_orgs_skip_the_rate_limit_check(self) -> None:
return_value=True,
) as mock_rate_limited,
patch(
"sentry.seer.agentic_triage.delivery.is_seer_seat_based_tier_enabled",
"sentry.seer.agentic_triage.delivery.is_seer_seat_or_token_billed_enabled",
return_value=True,
),
patch(
Expand Down
57 changes: 57 additions & 0 deletions tests/sentry/seer/autofix/test_autofix_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
from sentry.seer.autofix.trigger import is_issue_eligible_for_seer_automation
from sentry.seer.autofix.utils import (
MAX_SEER_REPOS_PER_PROJECT,
SEAT_BASED_STOPPING_POINTS,
USAGE_BASED_STOPPING_POINTS,
AutofixState,
AutofixStoppingPoint,
AutomationCodingAgent,
Expand All @@ -33,8 +35,11 @@
get_org_default_seer_automation_handoff,
get_repo_url_path,
get_token_based_billing_project_repos,
get_valid_automated_run_stopping_points,
has_project_connected_repos,
is_free_cohort_org,
is_seer_seat_based_tier_enabled,
is_seer_seat_or_token_billed_enabled,
read_preference_from_sentry_db,
replace_all_seer_project_repos,
update_seer_project_settings,
Expand Down Expand Up @@ -274,6 +279,58 @@ def test_returns_cached_value(self) -> None:
assert result is True


class TestIsSeerSeatOrTokenBilled(TestCase):
def setUp(self) -> None:
super().setUp()
self.organization = self.create_organization(name="test-org")

def tearDown(self) -> None:
super().tearDown()
cache.delete(f"seer:seat-based-tier:{self.organization.id}")

@with_feature("organizations:seat-based-seer-enabled")
def test_seat_based(self) -> None:
assert is_seer_seat_or_token_billed_enabled(self.organization) is True
assert get_valid_automated_run_stopping_points(self.organization) == (
SEAT_BASED_STOPPING_POINTS
)

@with_feature("organizations:token-billed-seer-enabled")
def test_token_billed(self) -> None:
assert is_seer_seat_or_token_billed_enabled(self.organization) is True
assert get_valid_automated_run_stopping_points(self.organization) == (
SEAT_BASED_STOPPING_POINTS
)

def test_legacy(self) -> None:
assert is_seer_seat_or_token_billed_enabled(self.organization) is False
assert get_valid_automated_run_stopping_points(self.organization) == (
USAGE_BASED_STOPPING_POINTS
)


class TestIsFreeCohortOrg(TestCase):
def setUp(self) -> None:
super().setUp()
self.organization = self.create_organization(name="test-org")
self.organization.update_option("agentic-triage-free-cohort", True)

def tearDown(self) -> None:
super().tearDown()
cache.delete(f"seer:seat-based-tier:{self.organization.id}")

def test_legacy(self) -> None:
assert is_free_cohort_org(self.organization) is True

@with_feature("organizations:seat-based-seer-enabled")
def test_seat_based(self) -> None:
assert is_free_cohort_org(self.organization) is False

@with_feature("organizations:token-billed-seer-enabled")
def test_token_billed(self) -> None:
assert is_free_cohort_org(self.organization) is False


class HasProjectConnectedReposBaseTest(TestCase):
"""Test the has_project_connected_repos function."""

Expand Down
6 changes: 3 additions & 3 deletions tests/sentry/seer/autofix/test_issue_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ def test_stopping_point_mapping(self, score: float, expected: AutofixStoppingPoi


@override_settings(SENTRY_SELF_HOSTED=False)
@patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True)
@patch("sentry.seer.autofix.issue_summary.is_seer_seat_or_token_billed_enabled", return_value=True)
class TestRunAutomationStoppingPoint(APITestCase, SnubaTestCase):
def setUp(self) -> None:
super().setUp()
Expand Down Expand Up @@ -1052,7 +1052,7 @@ def test_upper_bound_combinations(


@override_settings(SENTRY_SELF_HOSTED=False)
@patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True)
@patch("sentry.seer.autofix.issue_summary.is_seer_seat_or_token_billed_enabled", return_value=True)
class TestRunAutomationWithUpperBound(APITestCase, SnubaTestCase):
def setUp(self) -> None:
super().setUp()
Expand Down Expand Up @@ -1363,7 +1363,7 @@ def test_returns_false_when_rate_limited(
assert is_group_eligible_for_automation(self.group) is False


@patch("sentry.seer.autofix.issue_summary.is_seer_seat_based_tier_enabled", return_value=True)
@patch("sentry.seer.autofix.issue_summary.is_seer_seat_or_token_billed_enabled", return_value=True)
@override_settings(SENTRY_SELF_HOSTED=False)
class TestGetAutomationStoppingPoint(TestCase):
def setUp(self) -> None:
Expand Down
Loading
Loading