Skip to content
Open
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
26 changes: 26 additions & 0 deletions src/sentry/tasks/seer/agentic_triage/cron.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@
"organizations:seat-based-seer-enabled",
]

# Token billing makes the Seer quota check pass for every pay-as-you-go org, so
# scheduled runs stay with the orgs that already had them before the switch.
TOKEN_BILLING_CUTOFF = datetime(2026, 12, 8, tzinfo=UTC)
TOKEN_BILLING_PRIOR_RUN_WINDOW = timedelta(days=30)


class SeerAgenticTriageRunOptions(TypedDict):
"""Fully-resolved options for an agentic triage run. Persisted directly onto
Expand Down Expand Up @@ -365,6 +370,17 @@ def run_agentic_triage_for_org(
logger.info("night_shift.no_seer_quota", extra={"organization_id": organization.id})
return None

if (
schedule_id is not None
and features.has("organizations:token-billed-seer-enabled", organization)
and not _had_scheduled_run_before_token_billing(organization)
):
logger.info(
"night_shift.token_billed_without_prior_runs",
extra={"organization_id": organization.id},
)
return None

# Manual project runs scope to a single project, whose tweaks feed the run
# options; cron and org-wide manual runs have no single project.
single_project_id = project_ids[0] if project_ids and len(project_ids) == 1 else None
Expand Down Expand Up @@ -418,6 +434,16 @@ def run_agentic_triage_for_org(
return run.id


def _had_scheduled_run_before_token_billing(organization: Organization) -> bool:
return SeerWorkflowRun.objects.filter(
organization=organization,
workflow_config__strategy=SeerWorkflowStrategy.AGENTIC_TRIAGE,
trigger_key__isnull=False,
date_added__gte=TOKEN_BILLING_CUTOFF - TOKEN_BILLING_PRIOR_RUN_WINDOW,
date_added__lt=TOKEN_BILLING_CUTOFF,
).exists()


def _is_agentic_triage_enabled(organization: Organization) -> bool:
return options.get("seer.night_shift.enable") and features.has(
"organizations:seer-night-shift", organization
Expand Down
85 changes: 84 additions & 1 deletion tests/sentry/tasks/seer/test_agentic_triage.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import time
from collections.abc import Iterator
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
from inspect import unwrap
from typing import Any
from unittest.mock import Mock, patch
Expand All @@ -27,11 +27,14 @@
)
from sentry.seer.models.run import SeerRun, SeerRunMirrorStatus, SeerRunType
from sentry.seer.models.workflow import (
SeerWorkflowConfig,
SeerWorkflowRun,
SeerWorkflowRunExecution,
SeerWorkflowStrategy,
)
from sentry.tasks.seer.agentic_triage.cron import (
TOKEN_BILLING_CUTOFF,
TOKEN_BILLING_PRIOR_RUN_WINDOW,
AgenticTriageShardPlan,
ShardDispatchStatus,
_agentic_triage_cron_expr,
Expand Down Expand Up @@ -1008,6 +1011,86 @@ def test_free_cohort_skips_quota_check(self) -> None:
assert run_id is not None
mock_quota.assert_not_called()

def _create_prior_run(
self, org: Organization, *, date_added: datetime, trigger_key: str | None
) -> None:
run = SeerWorkflowRun.objects.create(
organization=org,
workflow_config=SeerWorkflowConfig.get_or_create_for_strategy(
organization_id=org.id, strategy=SeerWorkflowStrategy.AGENTIC_TRIAGE
),
trigger_key=trigger_key,
)
SeerWorkflowRun.objects.filter(id=run.id).update(date_added=date_added)

def test_token_billed_org_without_prior_runs_is_not_scheduled(self) -> None:
org = self.create_organization()
window_start = TOKEN_BILLING_CUTOFF - TOKEN_BILLING_PRIOR_RUN_WINDOW
self._create_prior_run(
org, date_added=window_start - timedelta(days=1), trigger_key="old-schedule"
)
self._create_prior_run(org, date_added=window_start + timedelta(days=1), trigger_key=None)
self._create_prior_run(
org, date_added=TOKEN_BILLING_CUTOFF + timedelta(days=1), trigger_key="after-cutoff"
)

with (
self.feature("organizations:token-billed-seer-enabled"),
patch(
"sentry.tasks.seer.agentic_triage.cron.quotas.backend.check_seer_quota",
return_value=True,
),
patch(
"sentry.tasks.seer.agentic_triage.cron.run_agentic_triage_execution"
) as mock_execution,
):
run_id = run_agentic_triage_for_org(org.id, schedule_id="2024-07-22T22:00")

assert run_id is None
assert not SeerWorkflowRun.objects.filter(trigger_key="2024-07-22T22:00").exists()
mock_execution.assert_not_called()

def test_token_billed_org_with_prior_scheduled_run_keeps_running(self) -> None:
org = self.create_organization()
self._create_prior_run(
org,
date_added=TOKEN_BILLING_CUTOFF - timedelta(days=1),
trigger_key="2024-07-21T22:00",
)

with (
self.feature("organizations:token-billed-seer-enabled"),
patch(
"sentry.tasks.seer.agentic_triage.cron.quotas.backend.check_seer_quota",
return_value=True,
),
patch(
"sentry.tasks.seer.agentic_triage.cron.run_agentic_triage_execution"
) as mock_execution,
):
run_id = run_agentic_triage_for_org(org.id, schedule_id="2024-07-22T22:00")

assert run_id is not None
mock_execution.assert_called_once()

def test_token_billed_manual_run_skips_prior_run_check(self) -> None:
org = self.create_organization()

with (
self.feature("organizations:token-billed-seer-enabled"),
patch(
"sentry.tasks.seer.agentic_triage.cron.quotas.backend.check_seer_quota",
return_value=True,
),
patch(
"sentry.tasks.seer.agentic_triage.cron.run_agentic_triage_execution"
) as mock_execution,
):
run_id = run_agentic_triage_for_org(org.id, options={"source": "manual"})

assert run_id is not None
mock_execution.assert_called_once()

def test_max_candidates_defaults_to_global_option(self) -> None:
org = self.create_organization()
low = self.create_project(organization=org, slug="low")
Expand Down
Loading