Skip to content

Commit cd8cea9

Browse files
chromygeorge-sentry
authored andcommitted
feat(autofix): Add analytics events for autofix phase start and completion (#112098)
Add per-phase analytics events for the autofix pipeline so we can track when each phase (root cause, solution, code changes, impact assessment, triage) starts and completes. Each phase gets its own event class (`ai.autofix.root_cause.started`, `ai.autofix.root_cause.completed`, etc.) plus a dedicated `ai.autofix.pr_created.completed` event. All events include `group_id`, `referrer`, `organization_id`, and `project_id`. Started events are recorded in `trigger_autofix_explorer` when a phase kicks off. Completed events are recorded in `AutofixOnCompletionHook._send_step_webhook` when the on-completion hook fires.
1 parent d7cfb40 commit cd8cea9

4 files changed

Lines changed: 173 additions & 2 deletions

File tree

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 | None
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: 25 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,21 @@ 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+
if completed_event_cls is not None:
253+
analytics.record(
254+
completed_event_cls(
255+
organization_id=organization.id,
256+
project_id=group.project_id,
257+
group_id=group.id,
258+
referrer=referrer,
259+
)
260+
)
238261

239262
@classmethod
240263
def _maybe_trigger_supergroups_embedding(

0 commit comments

Comments
 (0)