Skip to content

Commit 0e0a05a

Browse files
committed
feat(autofix): Add analytics events for autofix phase start and completion
Record per-phase analytics events when each autofix pipeline phase (root_cause, solution, code_changes) starts and completes. Each phase gets its own event class so they can be queried independently. Events include group_id, referrer, organization_id, and project_id. Started events fire in trigger_autofix_explorer, completed events fire in the on_completion_hook webhook handler. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Agent transcript: https://claudescope.sentry.dev/share/botteEgndhtvdoXxpJ2frzQPLVabdvt8jNcgx9NaA3k
1 parent fc84cb2 commit 0e0a05a

File tree

4 files changed

+172
-2
lines changed

4 files changed

+172
-2
lines changed

src/sentry/analytics/events/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .api_token_deleted import * # noqa: F401,F403
99
from .auth_v2 import * # noqa: F401,F403
1010
from .autofix_automation_events import * # noqa: F401,F403
11+
from .autofix_events import * # noqa: F401,F403
1112
from .checkin_processing_error_stored import * # noqa: F401,F403
1213
from .codeowners_assignment import * # noqa: F401,F403
1314
from .codeowners_created import * # noqa: F401,F403
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
from sentry import analytics
2+
3+
4+
@analytics.eventclass()
5+
class AiAutofixPhaseEvent(analytics.Event):
6+
organization_id: int
7+
project_id: int
8+
group_id: int
9+
referrer: str
10+
11+
12+
@analytics.eventclass("ai.autofix.root_cause.started")
13+
class AiAutofixRootCauseStartedEvent(AiAutofixPhaseEvent):
14+
pass
15+
16+
17+
@analytics.eventclass("ai.autofix.solution.started")
18+
class AiAutofixSolutionStartedEvent(AiAutofixPhaseEvent):
19+
pass
20+
21+
22+
@analytics.eventclass("ai.autofix.code_changes.started")
23+
class AiAutofixCodeChangesStartedEvent(AiAutofixPhaseEvent):
24+
pass
25+
26+
27+
@analytics.eventclass("ai.autofix.root_cause.completed")
28+
class AiAutofixRootCauseCompletedEvent(AiAutofixPhaseEvent):
29+
pass
30+
31+
32+
@analytics.eventclass("ai.autofix.solution.completed")
33+
class AiAutofixSolutionCompletedEvent(AiAutofixPhaseEvent):
34+
pass
35+
36+
37+
@analytics.eventclass("ai.autofix.code_changes.completed")
38+
class AiAutofixCodeChangesCompletedEvent(AiAutofixPhaseEvent):
39+
pass
40+
41+
42+
@analytics.eventclass("ai.autofix.impact_assessment.started")
43+
class AiAutofixImpactAssessmentStartedEvent(AiAutofixPhaseEvent):
44+
pass
45+
46+
47+
@analytics.eventclass("ai.autofix.impact_assessment.completed")
48+
class AiAutofixImpactAssessmentCompletedEvent(AiAutofixPhaseEvent):
49+
pass
50+
51+
52+
@analytics.eventclass("ai.autofix.triage.started")
53+
class AiAutofixTriageStartedEvent(AiAutofixPhaseEvent):
54+
pass
55+
56+
57+
@analytics.eventclass("ai.autofix.triage.completed")
58+
class AiAutofixTriageCompletedEvent(AiAutofixPhaseEvent):
59+
pass
60+
61+
62+
@analytics.eventclass("ai.autofix.pr_created.started")
63+
class AiAutofixPrCreatedStartedEvent(AiAutofixPhaseEvent):
64+
pass
65+
66+
67+
@analytics.eventclass("ai.autofix.pr_created.completed")
68+
class AiAutofixPrCreatedCompletedEvent(AiAutofixPhaseEvent):
69+
pass
70+
71+
72+
@analytics.eventclass("ai.autofix.agent_handoff")
73+
class AiAutofixAgentHandoffEvent(AiAutofixPhaseEvent):
74+
pass
75+
76+
77+
analytics.register(AiAutofixRootCauseStartedEvent)
78+
analytics.register(AiAutofixSolutionStartedEvent)
79+
analytics.register(AiAutofixCodeChangesStartedEvent)
80+
analytics.register(AiAutofixRootCauseCompletedEvent)
81+
analytics.register(AiAutofixSolutionCompletedEvent)
82+
analytics.register(AiAutofixCodeChangesCompletedEvent)
83+
analytics.register(AiAutofixImpactAssessmentStartedEvent)
84+
analytics.register(AiAutofixImpactAssessmentCompletedEvent)
85+
analytics.register(AiAutofixTriageStartedEvent)
86+
analytics.register(AiAutofixTriageCompletedEvent)
87+
analytics.register(AiAutofixPrCreatedStartedEvent)
88+
analytics.register(AiAutofixPrCreatedCompletedEvent)
89+
analytics.register(AiAutofixAgentHandoffEvent)

src/sentry/seer/autofix/autofix_agent.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@
88
from pydantic import BaseModel
99
from rest_framework.exceptions import PermissionDenied
1010

11+
from sentry import analytics
12+
from sentry.analytics.events.autofix_events import (
13+
AiAutofixAgentHandoffEvent,
14+
AiAutofixCodeChangesCompletedEvent,
15+
AiAutofixCodeChangesStartedEvent,
16+
AiAutofixImpactAssessmentCompletedEvent,
17+
AiAutofixImpactAssessmentStartedEvent,
18+
AiAutofixPhaseEvent,
19+
AiAutofixPrCreatedStartedEvent,
20+
AiAutofixRootCauseCompletedEvent,
21+
AiAutofixRootCauseStartedEvent,
22+
AiAutofixSolutionCompletedEvent,
23+
AiAutofixSolutionStartedEvent,
24+
AiAutofixTriageCompletedEvent,
25+
AiAutofixTriageStartedEvent,
26+
)
1127
from sentry.constants import ENABLE_SEER_CODING_DEFAULT
1228
from sentry.seer.autofix.artifact_schemas import (
1329
ImpactAssessmentArtifact,
@@ -82,34 +98,48 @@ def __init__(
8298
artifact_schema: type[BaseModel] | None,
8399
prompt_fn: Callable[..., str],
84100
enable_coding: bool = False,
101+
started_event: type[AiAutofixPhaseEvent] | None = None,
102+
completed_event: type[AiAutofixPhaseEvent] | None = None,
85103
):
86104
self.artifact_schema = artifact_schema
87105
self.prompt_fn = prompt_fn
88106
self.enable_coding = enable_coding
107+
self.started_event = started_event
108+
self.completed_event = completed_event
89109

90110

91111
# Step configurations mapping step to its artifact schema and prompt
92112
STEP_CONFIGS: dict[AutofixStep, StepConfig] = {
93113
AutofixStep.ROOT_CAUSE: StepConfig(
94114
artifact_schema=RootCauseArtifact,
95115
prompt_fn=root_cause_prompt,
116+
started_event=AiAutofixRootCauseStartedEvent,
117+
completed_event=AiAutofixRootCauseCompletedEvent,
96118
),
97119
AutofixStep.SOLUTION: StepConfig(
98120
artifact_schema=SolutionArtifact,
99121
prompt_fn=solution_prompt,
122+
started_event=AiAutofixSolutionStartedEvent,
123+
completed_event=AiAutofixSolutionCompletedEvent,
100124
),
101125
AutofixStep.CODE_CHANGES: StepConfig(
102126
artifact_schema=None, # Code changes read from file_patches
103127
prompt_fn=code_changes_prompt,
104128
enable_coding=True,
129+
started_event=AiAutofixCodeChangesStartedEvent,
130+
completed_event=AiAutofixCodeChangesCompletedEvent,
105131
),
106132
AutofixStep.IMPACT_ASSESSMENT: StepConfig(
107133
artifact_schema=ImpactAssessmentArtifact,
108134
prompt_fn=impact_assessment_prompt,
135+
started_event=AiAutofixImpactAssessmentStartedEvent,
136+
completed_event=AiAutofixImpactAssessmentCompletedEvent,
109137
),
110138
AutofixStep.TRIAGE: StepConfig(
111139
artifact_schema=TriageArtifact,
112140
prompt_fn=triage_prompt,
141+
started_event=AiAutofixTriageStartedEvent,
142+
completed_event=AiAutofixTriageCompletedEvent,
113143
),
114144
}
115145

@@ -215,6 +245,16 @@ def trigger_autofix_explorer(
215245
"""
216246

217247
config = STEP_CONFIGS[step]
248+
249+
if config.started_event is not None:
250+
analytics.record(
251+
config.started_event(
252+
organization_id=group.organization.id,
253+
project_id=group.project_id,
254+
group_id=group.id,
255+
referrer=referrer.value,
256+
)
257+
)
218258
client = get_autofix_explorer_client(
219259
group,
220260
intelligence_level=intelligence_level,
@@ -500,6 +540,15 @@ def trigger_coding_agent_handoff(
500540
auto_create_pr=auto_create_pr,
501541
)
502542

543+
analytics.record(
544+
AiAutofixAgentHandoffEvent(
545+
organization_id=group.organization.id,
546+
project_id=group.project_id,
547+
group_id=group.id,
548+
referrer=referrer.value,
549+
)
550+
)
551+
503552
metrics.incr(
504553
"autofix.explorer.trigger",
505554
tags={"step": "coding_agent_handoff", "referrer": referrer.value},
@@ -532,6 +581,15 @@ def trigger_push_changes(
532581
if group_id != group.id:
533582
raise SeerPermissionError("Unknown run id for group")
534583

584+
analytics.record(
585+
AiAutofixPrCreatedStartedEvent(
586+
organization_id=group.organization.id,
587+
project_id=group.project_id,
588+
group_id=group.id,
589+
referrer=referrer.value,
590+
)
591+
)
592+
535593
client.push_changes(
536594
run_id,
537595
repo_name=repo_name,

src/sentry/seer/autofix/on_completion_hook.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
from django.utils import timezone
77

8-
from sentry import features
8+
from sentry import analytics, features
9+
from sentry.analytics.events.autofix_events import AiAutofixPrCreatedCompletedEvent
910
from sentry.models.group import Group
1011
from sentry.models.organization import Organization
1112
from sentry.models.project import Project
1213
from sentry.seer.autofix.autofix_agent import (
14+
STEP_CONFIGS,
1315
AutofixStep,
1416
trigger_autofix_explorer,
1517
trigger_coding_agent_handoff,
@@ -136,6 +138,8 @@ def _send_step_webhook(
136138
# to find which step just completed
137139
webhook_action_type: SeerActionType | None = None
138140

141+
is_pr_created = False
142+
139143
if current_step is not None:
140144
artifact = cls.find_latest_artifact_for_step(state, current_step)
141145
if artifact is not None:
@@ -166,6 +170,15 @@ def _send_step_webhook(
166170
}
167171
for pull_request in state.repo_pr_states.values()
168172
]
173+
is_pr_created = True
174+
analytics.record(
175+
AiAutofixPrCreatedCompletedEvent(
176+
organization_id=organization.id,
177+
project_id=group.project_id,
178+
group_id=group.id,
179+
referrer=None if current_referrer is None else current_referrer.value,
180+
)
181+
)
169182
else:
170183
webhook_action_type = SeerActionType.CODING_COMPLETED
171184
diffs_by_repo = state.get_diffs_by_repo()
@@ -230,11 +243,20 @@ def _send_step_webhook(
230243
},
231244
)
232245

233-
if current_step is not None:
246+
if current_step is not None and not is_pr_created:
234247
referrer = current_referrer.value if current_referrer is not None else None
235248
metrics.incr(
236249
"autofix.explorer.complete", tags={"step": current_step.value, "referrer": referrer}
237250
)
251+
completed_event_cls = STEP_CONFIGS[current_step].completed_event
252+
analytics.record(
253+
completed_event_cls(
254+
organization_id=organization.id,
255+
project_id=group.project_id,
256+
group_id=group.id,
257+
referrer=referrer,
258+
)
259+
)
238260

239261
@classmethod
240262
def _maybe_trigger_supergroups_embedding(

0 commit comments

Comments
 (0)