11from __future__ import annotations
22
33import logging
4+ import random
5+ from datetime import UTC , datetime
6+ from uuid import uuid4
47
58import sentry_sdk
69from django .conf import settings
710from pydantic import BaseModel
811
912from sentry import options
13+ from sentry .issues .grouptype import LLMDetectedExperimentalGroupType
14+ from sentry .issues .issue_occurrence import IssueEvidence , IssueOccurrence
15+ from sentry .issues .producer import PayloadType , produce_occurrence_to_kafka
1016from sentry .models .project import Project
1117from sentry .net .http import connection_from_url
1218from sentry .seer .explorer .index_data import get_trace_for_transaction , get_transactions_for_project
2329SEER_TIMEOUT_S = 120
2430SEER_RETRIES = 1
2531
32+ NUM_TRANSACTIONS_TO_PROCESS = 10
33+
2634
2735seer_issue_detection_connection_pool = connection_from_url (
2836 settings .SEER_DEFAULT_URL ,
@@ -61,6 +69,79 @@ def __init__(
6169 self .error_message = error_message
6270
6371
72+ def create_issue_occurrence_from_detection (
73+ detected_issue : DetectedIssue ,
74+ trace : TraceData ,
75+ project_id : int ,
76+ transaction_name : str ,
77+ ) -> None :
78+ """
79+ Create and produce an IssueOccurrence from an LLM-detected issue.
80+ """
81+ event_id = uuid4 ().hex
82+ occurrence_id = uuid4 ().hex
83+ detection_time = datetime .now (UTC )
84+
85+ fingerprint = [f"llm-detected-{ detected_issue .title } -{ transaction_name } " ]
86+
87+ evidence_data = {
88+ "trace_id" : trace .trace_id ,
89+ "transaction" : transaction_name ,
90+ "explanation" : detected_issue .explanation ,
91+ "impact" : detected_issue .impact ,
92+ }
93+
94+ evidence_display = [
95+ IssueEvidence (name = "Explanation" , value = detected_issue .explanation , important = True ),
96+ IssueEvidence (name = "Impact" , value = detected_issue .impact , important = False ),
97+ IssueEvidence (name = "Evidence" , value = detected_issue .evidence , important = False ),
98+ ]
99+
100+ if detected_issue .missing_telemetry :
101+ evidence_display .append (
102+ IssueEvidence (
103+ name = "Missing Telemetry" ,
104+ value = detected_issue .missing_telemetry ,
105+ important = False ,
106+ )
107+ )
108+
109+ occurrence = IssueOccurrence (
110+ id = occurrence_id ,
111+ event_id = event_id ,
112+ project_id = project_id ,
113+ fingerprint = fingerprint ,
114+ issue_title = detected_issue .title ,
115+ subtitle = detected_issue .explanation [:200 ], # Truncate for subtitle
116+ resource_id = None ,
117+ evidence_data = evidence_data ,
118+ evidence_display = evidence_display ,
119+ type = LLMDetectedExperimentalGroupType ,
120+ detection_time = detection_time ,
121+ culprit = transaction_name ,
122+ level = "warning" ,
123+ )
124+
125+ event_data = {
126+ "event_id" : event_id ,
127+ "project_id" : project_id ,
128+ "platform" : "other" ,
129+ "received" : detection_time .isoformat (),
130+ "timestamp" : detection_time .isoformat (),
131+ "tags" : {
132+ "trace_id" : trace .trace_id ,
133+ "transaction" : transaction_name ,
134+ "llm_detected" : "true" ,
135+ },
136+ }
137+
138+ produce_occurrence_to_kafka (
139+ payload_type = PayloadType .OCCURRENCE ,
140+ occurrence = occurrence ,
141+ event_data = event_data ,
142+ )
143+
144+
64145def get_enabled_project_ids () -> list [int ]:
65146 """
66147 Get the list of project IDs that are explicitly enabled for LLM detection.
@@ -94,7 +175,7 @@ def run_llm_issue_detection() -> None:
94175@instrumented_task (
95176 name = "sentry.tasks.llm_issue_detection.detect_llm_issues_for_project" ,
96177 namespace = issues_tasks ,
97- processing_deadline_duration = 120 ,
178+ processing_deadline_duration = 300 ,
98179)
99180def detect_llm_issues_for_project (project_id : int ) -> None :
100181 """
@@ -106,6 +187,11 @@ def detect_llm_issues_for_project(project_id: int) -> None:
106187 transactions = get_transactions_for_project (
107188 project_id , limit = 50 , start_time_delta = {"minutes" : 30 }
108189 )
190+ if not transactions :
191+ return
192+
193+ # Sample a random subset of transactions to process
194+ transactions = random .sample (transactions , min (len (transactions ), NUM_TRANSACTIONS_TO_PROCESS ))
109195 for transaction in transactions :
110196 try :
111197 trace : TraceData | None = get_trace_for_transaction (
@@ -169,6 +255,17 @@ def detect_llm_issues_for_project(project_id: int) -> None:
169255 ),
170256 },
171257 )
258+ for detected_issue in response_data .issues :
259+ try :
260+ create_issue_occurrence_from_detection (
261+ detected_issue = detected_issue ,
262+ trace = trace ,
263+ project_id = project_id ,
264+ transaction_name = transaction .name ,
265+ )
266+
267+ except Exception as e :
268+ sentry_sdk .capture_exception (e )
172269 except LLMIssueDetectionError as e :
173270 sentry_sdk .capture_exception (e )
174- continue
271+ continue # if one transaction encounters an error, don't block processing of the others
0 commit comments