Skip to content

Commit a140f29

Browse files
roggenkemperpriscilawebdev
authored andcommitted
feat(detectors): Update detection algorithm for MN+1 Experimental Detector (#97533)
this pr makes a few changes to the MN+1 Experimental Detector: 1) it updates the `_equivalent` method to differentiate between spans that have the "default" op , which is common in Prisma. This made some detected issues be slightly off because it was finding a "pattern" with two spans that weren't necessarily the same 2) It updates the definition of a valid pattern to exclude those that start with a serialization span. serialization spans come after a query or operation, so it doesn't make sense to start on them 3) it keeps more of the most recent spans when leaving the continuing state which should allow for more accurate pattern finding. these changes should give more accurate results and fix issues we saw where the pattern was starting/ending 1-2 spans from where we expected it.
1 parent 918c2c1 commit a140f29

File tree

3 files changed

+288
-59
lines changed

3 files changed

+288
-59
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
{
2+
"event_id": "1997dd9e1f434f4d9ec638624dbfd8d7",
3+
"project": 2,
4+
"release": null,
5+
"dist": null,
6+
"platform": "node",
7+
"message": "",
8+
"datetime": "2025-08-08T16:00:00.582776+00:00",
9+
"tags": [
10+
["browser", "Firefox 137.0"],
11+
["browser.name", "Firefox"],
12+
["client_os", "Mac OS X 10.15"],
13+
["client_os.name", "Mac OS X"],
14+
["customerType", "small-plan"],
15+
["environment", "development"],
16+
["frontendSlowdown", "False"],
17+
["level", "info"],
18+
["os", "macOS 15.3.2"],
19+
["os.name", "macOS"],
20+
["runtime", "node v18.19.1"],
21+
["runtime.name", "node"],
22+
["user", "email:[email protected]"],
23+
["server_name", "MacBookPro"],
24+
["transaction", "GET /products"]
25+
],
26+
"culprit": "prisma:client:transaction",
27+
"environment": "production",
28+
"level": "info",
29+
"location": "prisma:client:transaction",
30+
"logger": "",
31+
"metadata": {
32+
"location": "prisma:client:transaction",
33+
"title": "prisma:client:transaction"
34+
},
35+
"spans": [
36+
{
37+
"timestamp": 1752683532.47828,
38+
"start_timestamp": 1752683532.371497,
39+
40+
"op": "default",
41+
"span_id": "a8b7c6d5e4f39210",
42+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
43+
"status": "ok",
44+
"description": "prisma:engine:query",
45+
"origin": "auto.db.otel.prisma",
46+
"data": {
47+
"sentry.origin": "auto.db.otel.prisma"
48+
},
49+
50+
"hash": "4808ca9185d0c670"
51+
},
52+
{
53+
"timestamp": 1752683532.394234,
54+
"start_timestamp": 1752683532.373677,
55+
56+
"op": "db",
57+
"span_id": "f3a2b1c0d9e87765",
58+
"parent_span_id": "a8b7c6d5e4f39210",
59+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
60+
"status": "ok",
61+
"description": "UPDATE users SET name = $1, email = $2 WHERE id = $3",
62+
"origin": "auto.db.otel.prisma",
63+
"data": {
64+
"db.system": "postgresql",
65+
"db.query.text": "UPDATE users SET name = $1, email = $2 WHERE id = $3",
66+
"otel.kind": "CLIENT",
67+
"sentry.op": "db",
68+
"sentry.origin": "auto.db.otel.prisma"
69+
},
70+
71+
"hash": "9747ee0db22ccb33"
72+
},
73+
{
74+
"timestamp": 1752683532.394473,
75+
"start_timestamp": 1752683532.394408,
76+
77+
"op": "default",
78+
"span_id": "a4b3c2d1e0f98876",
79+
"parent_span_id": "a8b7c6d5e4f39210",
80+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
81+
"status": "ok",
82+
"description": "prisma:engine:serialize",
83+
"origin": "auto.db.otel.prisma",
84+
"data": {
85+
"sentry.origin": "auto.db.otel.prisma"
86+
},
87+
88+
"hash": "ec7cb3896f17c6fd"
89+
},
90+
{
91+
"timestamp": 1752683532.415304,
92+
"start_timestamp": 1752683532.394817,
93+
94+
"op": "db",
95+
"span_id": "b5c4d3e2f1a09987",
96+
"parent_span_id": "a8b7c6d5e4f39210",
97+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
98+
"status": "ok",
99+
"description": "UPDATE users SET name = $1, email = $2 WHERE id = $3",
100+
"origin": "auto.db.otel.prisma",
101+
"data": {
102+
"db.system": "postgresql",
103+
"db.query.text": "UPDATE users SET name = $1, email = $2 WHERE id = $3",
104+
"otel.kind": "CLIENT",
105+
"sentry.op": "db",
106+
"sentry.origin": "auto.db.otel.prisma"
107+
},
108+
"hash": "9747ee0db22ccb33"
109+
},
110+
{
111+
"timestamp": 1752683532.415549,
112+
"start_timestamp": 1752683532.415481,
113+
114+
"op": "default",
115+
"span_id": "c6d5e4f3a2b1a098",
116+
"parent_span_id": "a8b7c6d5e4f39210",
117+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
118+
"status": "ok",
119+
"description": "prisma:engine:serialize",
120+
"origin": "auto.db.otel.prisma",
121+
"data": {
122+
"sentry.origin": "auto.db.otel.prisma"
123+
},
124+
125+
"hash": "ec7cb3896f17c6fd"
126+
},
127+
{
128+
"timestamp": 1752683532.436118,
129+
"start_timestamp": 1752683532.415906,
130+
131+
"op": "db",
132+
"span_id": "d7e6f5a4b3c2b1a9",
133+
"parent_span_id": "a8b7c6d5e4f39210",
134+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
135+
"status": "ok",
136+
"description": "UPDATE users SET name = $1, email = $2 WHERE id = $3",
137+
"origin": "auto.db.otel.prisma",
138+
"data": {
139+
"db.system": "postgresql",
140+
"db.query.text": "UPDATE users SET name = $1, email = $2 WHERE id = $3",
141+
"otel.kind": "CLIENT",
142+
"sentry.op": "db",
143+
"sentry.origin": "auto.db.otel.prisma"
144+
},
145+
146+
"hash": "9747ee0db22ccb33"
147+
},
148+
{
149+
"timestamp": 1752683532.436347,
150+
"start_timestamp": 1752683532.436282,
151+
152+
"op": "default",
153+
"span_id": "e8f7a6b5c4d3c2ba",
154+
"parent_span_id": "a8b7c6d5e4f39210",
155+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
156+
"status": "ok",
157+
"description": "prisma:engine:serialize",
158+
"origin": "auto.db.otel.prisma",
159+
"data": {
160+
"sentry.origin": "auto.db.otel.prisma"
161+
},
162+
163+
"hash": "ec7cb3896f17c6fd"
164+
},
165+
{
166+
"timestamp": 1752683532.457153,
167+
"start_timestamp": 1752683532.436713,
168+
169+
"op": "db",
170+
"span_id": "f9a8b7c6d5e4d3cb",
171+
"parent_span_id": "a8b7c6d5e4f39210",
172+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
173+
"status": "ok",
174+
"description": "UPDATE users SET name = $1, email = $2 WHERE id = $3",
175+
"origin": "auto.db.otel.prisma",
176+
"data": {
177+
"db.system": "postgresql",
178+
"db.query.text": "UPDATE users SET name = $1, email = $2 WHERE id = $3",
179+
"otel.kind": "CLIENT",
180+
"sentry.op": "db",
181+
"sentry.origin": "auto.db.otel.prisma"
182+
},
183+
184+
"hash": "9747ee0db22ccb33"
185+
},
186+
{
187+
"timestamp": 1752683532.457415,
188+
"start_timestamp": 1752683532.457346,
189+
190+
"op": "default",
191+
"span_id": "a0b9c8d7e6f5e4dc",
192+
"parent_span_id": "a8b7c6d5e4f39210",
193+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
194+
"status": "ok",
195+
"description": "prisma:engine:serialize",
196+
"origin": "auto.db.otel.prisma",
197+
"data": {
198+
"sentry.origin": "auto.db.otel.prisma"
199+
},
200+
201+
"hash": "ec7cb3896f17c6fd"
202+
},
203+
{
204+
"timestamp": 1752683532.477919,
205+
"start_timestamp": 1752683532.457749,
206+
207+
"op": "db",
208+
"span_id": "b1c0d9e8f7a6f5ed",
209+
"parent_span_id": "a8b7c6d5e4f39210",
210+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
211+
"status": "ok",
212+
"description": "UPDATE users SET name = $1, email = $2 WHERE id = $3",
213+
"origin": "auto.db.otel.prisma",
214+
"data": {
215+
"db.system": "postgresql",
216+
"db.query.text": "UPDATE users SET name = $1, email = $2 WHERE id = $3",
217+
"otel.kind": "CLIENT",
218+
"sentry.op": "db",
219+
"sentry.origin": "auto.db.otel.prisma"
220+
},
221+
222+
"hash": "9747ee0db22ccb33"
223+
},
224+
{
225+
"timestamp": 1752683532.478153,
226+
"start_timestamp": 1752683532.478087,
227+
228+
"op": "default",
229+
"span_id": "c2d1e0f9a8b7a6fe",
230+
"parent_span_id": "a8b7c6d5e4f39210",
231+
"trace_id": "a1b2c3d4e5f67890abcdef1234567890",
232+
"status": "ok",
233+
"description": "prisma:engine:serialize",
234+
"origin": "auto.db.otel.prisma",
235+
"data": {
236+
"sentry.origin": "auto.db.otel.prisma"
237+
},
238+
239+
"hash": "ec7cb3896f17c6fd"
240+
}
241+
]
242+
}

src/sentry/performance_issues/detectors/experiments/mn_plus_one_db_span_detector.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ def _equivalent(self, a: Span, b: Span) -> bool:
4343
if not first_op or not second_op or first_op != second_op:
4444
return False
4545

46+
if first_op == "default":
47+
return a.get("description") == b.get("description")
48+
4649
if first_op.startswith("db"):
4750
return a.get("hash") == b.get("hash")
4851

@@ -119,6 +122,14 @@ def _is_valid_pattern(self, pattern: Sequence[Span]) -> bool:
119122
found_db_op = False
120123
found_different_span = False
121124

125+
# Patterns shouldn't start with a serialize span, since that follows an operation or query.
126+
first_span_description = pattern[0].get("description", "")
127+
if (
128+
first_span_description == "prisma:client:serialize"
129+
or first_span_description == "prisma:engine:serialize"
130+
):
131+
return False
132+
122133
for span in pattern:
123134
op = span.get("op") or ""
124135
description = span.get("description") or ""
@@ -187,9 +198,14 @@ def next(self, span: Span) -> tuple[MNPlusOneState, PerformanceProblem | None]:
187198

188199
# We've broken the MN pattern, so return to the Searching state. If it
189200
# is a significant problem, also return a PerformanceProblem.
190-
times_occurred = int(len(self.spans) / len(self.pattern))
191-
start_index = len(self.pattern) * times_occurred
192-
remaining_spans = self.spans[start_index:] + [span]
201+
202+
# Keep more context for pattern detection by including spans that could be
203+
# the beginning of a new pattern. Instead of just keeping the incomplete
204+
# remainder, keep the last pattern_length spans plus the current span.
205+
# Keep at least the last pattern_length spans (or all if we have fewer)
206+
pattern_length = len(self.pattern)
207+
context_start = max(0, len(self.spans) - pattern_length)
208+
remaining_spans = self.spans[context_start:] + [span]
193209
return (
194210
SearchingForMNPlusOne(
195211
settings=self.settings,

0 commit comments

Comments
 (0)