From 35972c8612c6f6140611b60a56cabda18555427a Mon Sep 17 00:00:00 2001 From: Richard Roggenkemper Date: Thu, 16 Oct 2025 13:54:20 -0700 Subject: [PATCH 1/4] add base task for llm detection --- src/sentry/conf/server.py | 4 ++ src/sentry/tasks/llm_issue_detection.py | 59 +++++++++++++++++++ .../sentry/tasks/test_llm_issue_detection.py | 23 ++++++++ 3 files changed, 86 insertions(+) create mode 100644 src/sentry/tasks/llm_issue_detection.py create mode 100644 tests/sentry/tasks/test_llm_issue_detection.py diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 1cb0dd5b7c3b78..ba9685c811bb2e 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -1102,6 +1102,10 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "task": "ai_agent_monitoring:sentry.tasks.ai_agent_monitoring.fetch_ai_model_costs", "schedule": task_crontab("*/30", "*", "*", "*", "*"), }, + "llm-issue-detection": { + "task": "issues:sentry.tasks.llm_issue_detection.run_llm_issue_detection", + "schedule": task_crontab("*/30", "*", "*", "*", "*"), + }, "preprod-detect-expired-artifacts": { "task": "preprod:sentry.preprod.tasks.detect_expired_preprod_artifacts", "schedule": task_crontab("0", "*", "*", "*", "*"), diff --git a/src/sentry/tasks/llm_issue_detection.py b/src/sentry/tasks/llm_issue_detection.py new file mode 100644 index 00000000000000..359b04dbd88823 --- /dev/null +++ b/src/sentry/tasks/llm_issue_detection.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +import logging + +from sentry import options +from sentry.models.project import Project +from sentry.tasks.base import instrumented_task +from sentry.taskworker.namespaces import issues_tasks + +logger = logging.getLogger("sentry.tasks.llm_issue_detection") + + +def get_enabled_project_ids() -> list[int]: + """ + Get the list of project IDs that are explicitly enabled for LLM detection. + + Returns the allowlist from system options. + """ + return options.get("issue-detection.llm-detection.projects-allowlist") + + +@instrumented_task( + name="sentry.tasks.llm_issue_detection.run_llm_issue_detection", + namespace=issues_tasks, + processing_deadline_duration=120, +) +def run_llm_issue_detection() -> None: + """ + Main scheduled task for LLM issue detection. + """ + if not options.get("issue-detection.llm-detection.enabled"): + return + + enabled_project_ids = get_enabled_project_ids() + if not enabled_project_ids: + return + + projects = Project.objects.filter( + id__in=enabled_project_ids, + ) + + for project in projects: + try: + process_project(project) + except Exception: + logger.exception( + "Failed to process project for LLM detection", + extra={"project_id": project.id, "org_id": project.organization_id}, + ) + + +def process_project(project: Project) -> None: + """ + Process a single project for LLM issue detection. + """ + logger.info( + "Processing project for LLM detection", + extra={"project_id": project.id}, + ) diff --git a/tests/sentry/tasks/test_llm_issue_detection.py b/tests/sentry/tasks/test_llm_issue_detection.py new file mode 100644 index 00000000000000..e521a398918919 --- /dev/null +++ b/tests/sentry/tasks/test_llm_issue_detection.py @@ -0,0 +1,23 @@ +from unittest.mock import patch + +from sentry.tasks.llm_issue_detection import run_llm_issue_detection +from sentry.testutils.cases import TestCase + + +class LLMIssueDetectionTest(TestCase): + @patch("sentry.tasks.llm_issue_detection.process_project") + def test_run_detection_processes_enabled_projects(self, mock_process): + """Test run_detection processes enabled projects.""" + project = self.create_project() + + with self.options( + { + "issue-detection.llm-detection.enabled": True, + "issue-detection.llm-detection.projects-allowlist": [project.id], + } + ): + run_llm_issue_detection() + + assert mock_process.called + call_args = mock_process.call_args[0] + assert call_args[0].id == project.id From dfbad402c6c9d0bc4eaccc847af7d3972315b412 Mon Sep 17 00:00:00 2001 From: Richard Roggenkemper Date: Thu, 16 Oct 2025 14:01:32 -0700 Subject: [PATCH 2/4] add codeowners --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ce3ebab00649e3..79aca75176b2d0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -572,6 +572,7 @@ pnpm-lock.yaml @getsentry/owners-js-de /src/sentry/tasks/post_process.py @getsentry/issue-detection-backend /src/sentry/tasks/unmerge.py @getsentry/issue-detection-backend /src/sentry/tasks/weekly_escalating_forecast.py @getsentry/issue-detection-backend +/src/sentry/tasks/llm_issue_detection.py @getsentry/issue-detection-backend /static/app/components/events/contexts/ @getsentry/issue-workflow /static/app/components/events/eventTags/ @getsentry/issue-workflow /static/app/components/events/highlights/ @getsentry/issue-workflow @@ -612,6 +613,7 @@ pnpm-lock.yaml @getsentry/owners-js-de /tests/sentry/tasks/test_merge.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_post_process.py @getsentry/issue-detection-backend /tests/sentry/tasks/test_weekly_escalating_forecast.py @getsentry/issue-detection-backend +/tests/sentry/tasks/test_llm_issue_detection.py @getsentry/issue-detection-backend /tests/snuba/search/ @getsentry/issue-workflow ## End of Issues From 4058110a5168e3059e1f44648ab6f4f606e38e9e Mon Sep 17 00:00:00 2001 From: Richard Roggenkemper Date: Fri, 17 Oct 2025 12:02:40 -0700 Subject: [PATCH 3/4] fix test --- src/sentry/conf/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index ba9685c811bb2e..af2c452b2c7d85 100644 --- a/src/sentry/conf/server.py +++ b/src/sentry/conf/server.py @@ -895,6 +895,7 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str: "sentry.tasks.email", "sentry.tasks.embeddings_grouping.backfill_seer_grouping_records_for_project", "sentry.tasks.groupowner", + "sentry.tasks.llm_issue_detection", "sentry.tasks.merge", "sentry.tasks.on_demand_metrics", "sentry.tasks.options", From 0776ccaacaef8a2108a025bf037f980cb9eea528 Mon Sep 17 00:00:00 2001 From: Richard Roggenkemper Date: Fri, 17 Oct 2025 15:37:44 -0700 Subject: [PATCH 4/4] create second task --- src/sentry/tasks/llm_issue_detection.py | 26 +++++++++---------- .../sentry/tasks/test_llm_issue_detection.py | 11 ++++---- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/sentry/tasks/llm_issue_detection.py b/src/sentry/tasks/llm_issue_detection.py index 359b04dbd88823..17bfc563922cf2 100644 --- a/src/sentry/tasks/llm_issue_detection.py +++ b/src/sentry/tasks/llm_issue_detection.py @@ -35,25 +35,23 @@ def run_llm_issue_detection() -> None: if not enabled_project_ids: return - projects = Project.objects.filter( - id__in=enabled_project_ids, - ) - - for project in projects: - try: - process_project(project) - except Exception: - logger.exception( - "Failed to process project for LLM detection", - extra={"project_id": project.id, "org_id": project.organization_id}, - ) + # Spawn a sub-task for each project + for project_id in enabled_project_ids: + detect_llm_issues_for_project.delay(project_id) -def process_project(project: Project) -> None: +@instrumented_task( + name="sentry.tasks.llm_issue_detection.detect_llm_issues_for_project", + namespace=issues_tasks, + processing_deadline_duration=120, +) +def detect_llm_issues_for_project(project_id: int) -> None: """ Process a single project for LLM issue detection. """ + project = Project.objects.get(id=project_id) + logger.info( "Processing project for LLM detection", - extra={"project_id": project.id}, + extra={"project_id": project.id, "org_id": project.organization_id}, ) diff --git a/tests/sentry/tasks/test_llm_issue_detection.py b/tests/sentry/tasks/test_llm_issue_detection.py index e521a398918919..9f72e4d715044d 100644 --- a/tests/sentry/tasks/test_llm_issue_detection.py +++ b/tests/sentry/tasks/test_llm_issue_detection.py @@ -5,9 +5,9 @@ class LLMIssueDetectionTest(TestCase): - @patch("sentry.tasks.llm_issue_detection.process_project") - def test_run_detection_processes_enabled_projects(self, mock_process): - """Test run_detection processes enabled projects.""" + @patch("sentry.tasks.llm_issue_detection.detect_llm_issues_for_project.delay") + def test_run_detection_dispatches_sub_tasks(self, mock_delay): + """Test run_detection spawns sub-tasks for each project.""" project = self.create_project() with self.options( @@ -18,6 +18,5 @@ def test_run_detection_processes_enabled_projects(self, mock_process): ): run_llm_issue_detection() - assert mock_process.called - call_args = mock_process.call_args[0] - assert call_args[0].id == project.id + assert mock_delay.called + assert mock_delay.call_args[0][0] == project.id