Skip to content

Commit 23c1751

Browse files
committed
feat: add performance event formatting for N+1 query detection (#345)
Add comprehensive support for formatting performance issues, particularly N+1 database query detection. Includes evidence data parsing, span tree visualization, and test fixtures for performance events. - Add performance event fixture with N+1 query occurrence data - Extend API schema to support performance issue metadata - Implement formatPerformanceIssueOutput with N+1 query detection - Add span tree visualization for performance issues - Include comprehensive tests for performance formatting Fixes #345
1 parent 0859772 commit 23c1751

File tree

6 files changed

+1118
-6
lines changed

6 files changed

+1118
-6
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
{
2+
"id": "a1b2c3d4e5f6789012345678901234567",
3+
"groupID": "7890123456",
4+
"eventID": "a1b2c3d4e5f6789012345678901234567",
5+
"projectID": "4509062593708032",
6+
"size": 8432,
7+
"type": "transaction",
8+
"title": "GET /api/users",
9+
"message": "",
10+
"platform": "python",
11+
"datetime": "2025-08-06T18:00:00.000000Z",
12+
"dateCreated": "2025-08-06T18:00:00.000000Z",
13+
"contexts": {
14+
"trace": {
15+
"trace_id": "abcdef1234567890abcdef1234567890",
16+
"span_id": "1234567890abcdef",
17+
"op": "http.server",
18+
"status": "ok",
19+
"exclusive_time": 250.5,
20+
"data": {
21+
"http.request.method": "GET",
22+
"url.path": "/api/users"
23+
}
24+
}
25+
},
26+
"occurrence": {
27+
"id": "occ_123456789",
28+
"projectId": 4509062593708032,
29+
"eventId": "a1b2c3d4e5f6789012345678901234567",
30+
"fingerprint": [
31+
"n_plus_one_db_queries",
32+
"SELECT * FROM users WHERE id = %s"
33+
],
34+
"issueTitle": "N+1 Query: SELECT * FROM users WHERE id = %s",
35+
"subtitle": "Database query repeated 25 times",
36+
"resourceId": null,
37+
"type": 1006,
38+
"detectionTime": 1722963600,
39+
"level": "warning",
40+
"culprit": "SELECT * FROM users WHERE id = %s",
41+
"priority": 50,
42+
"assignee": null,
43+
"evidenceData": {
44+
"transactionName": "/api/users",
45+
"parentSpan": "GET /api/users",
46+
"parentSpanIds": ["parent123"],
47+
"repeatingSpansCompact": "SELECT * FROM users WHERE id = %s",
48+
"repeatingSpansDescription": "SELECT * FROM users WHERE id = %s",
49+
"repeatingSpansCount": 25,
50+
"numberRepeatingSpans": "25",
51+
"offenderSpanIds": [
52+
"span001",
53+
"span002",
54+
"span003",
55+
"span004",
56+
"span005",
57+
"span006",
58+
"span007",
59+
"span008",
60+
"span009",
61+
"span010",
62+
"span011",
63+
"span012",
64+
"span013",
65+
"span014",
66+
"span015",
67+
"span016",
68+
"span017",
69+
"span018",
70+
"span019",
71+
"span020",
72+
"span021",
73+
"span022",
74+
"span023",
75+
"span024",
76+
"span025"
77+
],
78+
"op": "db"
79+
},
80+
"evidenceDisplay": [
81+
{
82+
"name": "Offending Spans",
83+
"value": "SELECT * FROM users WHERE id = %s",
84+
"important": true
85+
},
86+
{
87+
"name": "Repeated",
88+
"value": "25 times",
89+
"important": true
90+
}
91+
]
92+
},
93+
"entries": [
94+
{
95+
"type": "spans",
96+
"data": [
97+
{
98+
"span_id": "parent123",
99+
"trace_id": "abcdef1234567890abcdef1234567890",
100+
"parent_span_id": "1234567890abcdef",
101+
"op": "http.server",
102+
"description": "GET /api/users",
103+
"status": "ok",
104+
"start_timestamp": 1722963600.0,
105+
"timestamp": 1722963600.25,
106+
"data": {
107+
"http.request.method": "GET",
108+
"url.path": "/api/users"
109+
}
110+
},
111+
{
112+
"span_id": "span001",
113+
"trace_id": "abcdef1234567890abcdef1234567890",
114+
"parent_span_id": "parent123",
115+
"op": "db.query",
116+
"description": "SELECT * FROM users WHERE id = 1",
117+
"status": "ok",
118+
"start_timestamp": 1722963600.01,
119+
"timestamp": 1722963600.015,
120+
"data": {
121+
"db.system": "postgresql",
122+
"db.operation": "SELECT"
123+
}
124+
},
125+
{
126+
"span_id": "span002",
127+
"trace_id": "abcdef1234567890abcdef1234567890",
128+
"parent_span_id": "parent123",
129+
"op": "db.query",
130+
"description": "SELECT * FROM users WHERE id = 2",
131+
"status": "ok",
132+
"start_timestamp": 1722963600.02,
133+
"timestamp": 1722963600.025,
134+
"data": {
135+
"db.system": "postgresql",
136+
"db.operation": "SELECT"
137+
}
138+
},
139+
{
140+
"span_id": "span003",
141+
"trace_id": "abcdef1234567890abcdef1234567890",
142+
"parent_span_id": "parent123",
143+
"op": "db.query",
144+
"description": "SELECT * FROM users WHERE id = 3",
145+
"status": "ok",
146+
"start_timestamp": 1722963600.03,
147+
"timestamp": 1722963600.035,
148+
"data": {
149+
"db.system": "postgresql",
150+
"db.operation": "SELECT"
151+
}
152+
},
153+
{
154+
"span_id": "span004",
155+
"trace_id": "abcdef1234567890abcdef1234567890",
156+
"parent_span_id": "parent123",
157+
"op": "db.query",
158+
"description": "SELECT * FROM users WHERE id = 4",
159+
"status": "ok",
160+
"start_timestamp": 1722963600.04,
161+
"timestamp": 1722963600.045,
162+
"data": {
163+
"db.system": "postgresql",
164+
"db.operation": "SELECT"
165+
}
166+
},
167+
{
168+
"span_id": "span005",
169+
"trace_id": "abcdef1234567890abcdef1234567890",
170+
"parent_span_id": "parent123",
171+
"op": "db.query",
172+
"description": "SELECT * FROM users WHERE id = 5",
173+
"status": "ok",
174+
"start_timestamp": 1722963600.05,
175+
"timestamp": 1722963600.055,
176+
"data": {
177+
"db.system": "postgresql",
178+
"db.operation": "SELECT"
179+
}
180+
}
181+
]
182+
},
183+
{
184+
"type": "request",
185+
"data": {
186+
"method": "GET",
187+
"url": "https://api.example.com/api/users",
188+
"query": [],
189+
"headers": [["Accept", "application/json"], ["Host", "api.example.com"]]
190+
}
191+
}
192+
],
193+
"tags": [
194+
{ "key": "environment", "value": "production" },
195+
{ "key": "transaction", "value": "/api/users" }
196+
]
197+
}

packages/mcp-server-mocks/src/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ import autofixStateFixture from "./fixtures/autofix-state.json" with {
2929
};
3030
import issueFixture from "./fixtures/issue.json" with { type: "json" };
3131
import eventsFixture from "./fixtures/event.json" with { type: "json" };
32+
import performanceEventFixture from "./fixtures/performance-event.json" with {
33+
type: "json",
34+
};
3235
import eventAttachmentsFixture from "./fixtures/event-attachments.json" with {
3336
type: "json",
3437
};
@@ -809,6 +812,23 @@ export const restHandlers = buildHandlers([
809812
fetch: () => HttpResponse.json(eventsFixture),
810813
},
811814

815+
// Performance issue with N+1 query detection
816+
{
817+
method: "get",
818+
path: "/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/",
819+
fetch: () => HttpResponse.json(performanceEventFixture),
820+
},
821+
{
822+
method: "get",
823+
path: "/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/",
824+
fetch: () => HttpResponse.json(performanceEventFixture),
825+
},
826+
{
827+
method: "get",
828+
path: "/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/a1b2c3d4e5f6789012345678901234567/",
829+
fetch: () => HttpResponse.json(performanceEventFixture),
830+
},
831+
812832
{
813833
method: "get",
814834
path: "/api/0/organizations/sentry-mcp-evals/releases/",

packages/mcp-server/src/api-client/schema.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,15 @@ export const IssueSchema = z.object({
193193
culprit: z.string(),
194194
type: z.union([z.literal("error"), z.literal("transaction"), z.unknown()]),
195195
assignedTo: AssignedToSchema.optional(),
196+
issueType: z.string().optional(),
197+
issueCategory: z.string().optional(),
198+
metadata: z
199+
.object({
200+
title: z.string().optional(),
201+
location: z.string().optional(),
202+
value: z.string().optional(),
203+
})
204+
.optional(),
196205
});
197206

198207
export const IssueListSchema = z.array(IssueSchema);
@@ -371,10 +380,33 @@ export const TransactionEventSchema = BaseEventSchema.omit({
371380
type: true,
372381
}).extend({
373382
type: z.literal("transaction"),
374-
occurrence: z.object({
375-
issueTitle: z.string(),
376-
culprit: z.string().nullable(),
377-
}),
383+
occurrence: z
384+
.object({
385+
id: z.string().optional(),
386+
projectId: z.number().optional(),
387+
eventId: z.string().optional(),
388+
fingerprint: z.array(z.string()).optional(),
389+
issueTitle: z.string(),
390+
subtitle: z.string().optional(),
391+
resourceId: z.string().nullable().optional(),
392+
evidenceData: z.record(z.string(), z.any()).optional(),
393+
evidenceDisplay: z
394+
.array(
395+
z.object({
396+
name: z.string(),
397+
value: z.string(),
398+
important: z.boolean().optional(),
399+
}),
400+
)
401+
.optional(),
402+
type: z.number().optional(),
403+
detectionTime: z.number().optional(),
404+
level: z.string().optional(),
405+
culprit: z.string().nullable(),
406+
priority: z.number().optional(),
407+
assignee: z.string().nullable().optional(),
408+
})
409+
.optional(),
378410
});
379411

380412
export const UnknownEventSchema = BaseEventSchema.omit({

0 commit comments

Comments
 (0)