diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c9373994f1a0db..27f4dd7abf67a5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -575,6 +575,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 @@ -615,6 +616,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 diff --git a/src/sentry/conf/server.py b/src/sentry/conf/server.py index 5263f638dd01fb..cd5e874243d561 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", @@ -1102,6 +1103,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..17bfc563922cf2 --- /dev/null +++ b/src/sentry/tasks/llm_issue_detection.py @@ -0,0 +1,57 @@ +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 + + # Spawn a sub-task for each project + for project_id in enabled_project_ids: + detect_llm_issues_for_project.delay(project_id) + + +@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, "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 new file mode 100644 index 00000000000000..9f72e4d715044d --- /dev/null +++ b/tests/sentry/tasks/test_llm_issue_detection.py @@ -0,0 +1,22 @@ +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.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( + { + "issue-detection.llm-detection.enabled": True, + "issue-detection.llm-detection.projects-allowlist": [project.id], + } + ): + run_llm_issue_detection() + + assert mock_delay.called + assert mock_delay.call_args[0][0] == project.id