From 83ce7aea6852d6d589e61cda605f29da15b7ed12 Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Thu, 16 Apr 2026 17:45:59 -0400 Subject: [PATCH 1/2] feat(seer): Print Explorer URL after night shift trigger script runs The trigger-night-shift script now outputs a clickable Explorer URL after a run completes, making it easy to jump straight to the triage session for debugging. Falls back to printing the raw run ID if the URL prefix isn't configured. To support this, run_night_shift_for_org now returns the agent_run_id. The return value is ignored when dispatched via Celery. --- bin/seer/trigger-night-shift | 24 ++++++++++++++++++++++- src/sentry/tasks/seer/night_shift/cron.py | 4 +++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/bin/seer/trigger-night-shift b/bin/seer/trigger-night-shift index 606d9c7f0deff7..04b80ce8ec1dfa 100755 --- a/bin/seer/trigger-night-shift +++ b/bin/seer/trigger-night-shift @@ -7,6 +7,8 @@ configure() import argparse import sys +from sentry import options +from sentry.models.organization import Organization from sentry.tasks.seer.night_shift.cron import run_night_shift_for_org @@ -17,11 +19,31 @@ def _positive_int(value: str) -> int: return parsed +def _get_explorer_url(org_id: int, run_id: int) -> str | None: + url_prefix = options.get("system.url-prefix") + if not url_prefix: + return None + + try: + slug = Organization.objects.values_list("slug", flat=True).get(id=org_id) + except Organization.DoesNotExist: + return None + + return f"{url_prefix}/organizations/{slug}/issues/?explorerRunId={run_id}" + + def main(org_id: int, max_candidates: int | None) -> None: sys.stdout.write(f"> Running night shift for organization {org_id}...\n") - run_night_shift_for_org(org_id, max_candidates=max_candidates) + agent_run_id = run_night_shift_for_org(org_id, max_candidates=max_candidates) sys.stdout.write("> Done.\n") + if agent_run_id is not None: + explorer_url = _get_explorer_url(org_id, agent_run_id) + if explorer_url: + sys.stdout.write(f"> Explorer run: {explorer_url}\n") + else: + sys.stdout.write(f"> Explorer run ID: {agent_run_id}\n") + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Trigger night shift for an organization.") diff --git a/src/sentry/tasks/seer/night_shift/cron.py b/src/sentry/tasks/seer/night_shift/cron.py index 9b2215fbe00c04..849b6878217d10 100644 --- a/src/sentry/tasks/seer/night_shift/cron.py +++ b/src/sentry/tasks/seer/night_shift/cron.py @@ -91,7 +91,7 @@ def run_night_shift_for_org( organization_id: int, dry_run: bool = False, max_candidates: int | None = None, -) -> None: +) -> int | None: try: organization = Organization.objects.get( id=organization_id, status=OrganizationStatus.ACTIVE @@ -205,6 +205,8 @@ def run_night_shift_for_org( autofix_triggered += 1 sentry_sdk.metrics.count("night_shift.autofix_triggered", autofix_triggered) + return agent_run_id + def _get_eligible_orgs_from_batch( orgs: Sequence[Organization], From 7121412d9c968f6f76ae242560e7cc15ad4ee42f Mon Sep 17 00:00:00 2001 From: Trevor Elkins Date: Thu, 16 Apr 2026 18:00:30 -0400 Subject: [PATCH 2/2] fix(seer): Use explicit return None for mypy strictness Bare `return` in `run_night_shift_for_org` (annotated `-> int | None`) tripped mypy's `[return-value]` check in CI. Make the early exits explicit with `return None`. Co-Authored-By: Claude --- src/sentry/tasks/seer/night_shift/cron.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sentry/tasks/seer/night_shift/cron.py b/src/sentry/tasks/seer/night_shift/cron.py index 849b6878217d10..19295af944764e 100644 --- a/src/sentry/tasks/seer/night_shift/cron.py +++ b/src/sentry/tasks/seer/night_shift/cron.py @@ -97,7 +97,7 @@ def run_night_shift_for_org( id=organization_id, status=OrganizationStatus.ACTIVE ) except Organization.DoesNotExist: - return + return None sentry_sdk.set_tags( { @@ -118,7 +118,7 @@ def run_night_shift_for_org( "organization_slug": organization.slug, }, ) - return + return None except Exception: logger.exception( "night_shift.failed_to_get_eligible_projects", @@ -126,7 +126,7 @@ def run_night_shift_for_org( "organization_id": organization_id, }, ) - return + return None sentry_sdk.metrics.distribution("night_shift.eligible_projects", len(eligible_projects)) @@ -168,7 +168,7 @@ def run_night_shift_for_org( }, ) run.update(error_message="Night shift run failed") - return + return None sentry_sdk.metrics.distribution("night_shift.candidates_selected", len(candidates)) for c in candidates: