fix(asyncpg): Use distinct span ops for cursor iteration and fetch to prevent N+1 false positives#6609
Conversation
… prevent N+1 false positives When asyncpg iterates a server-side cursor (via CursorIterator.__anext__) or fetches rows in batches (via Cursor.fetch), each batch triggers BaseCursor._bind_exec or BaseCursor._exec, producing a burst of identical spans. Sentry's N+1 detector incorrectly flags these as N+1 queries. Set `sentry.op` to `db.cursor.iter` for CursorIterator-driven spans and `db.cursor.fetch` for Cursor.fetch-driven spans so the backend can exclude them from N+1 detection. Adds span_op_override_value parameter to record_sql_queries to propagate the override without changing existing callers. This will require changes on the backend in order to not trigger n+1 alerts on these ops. Fixes #6576 Fixes PY-2534
Codecov Results 📊✅ 92048 passed | ⏭️ 6140 skipped | Total: 98188 | Pass Rate: 93.75% | Execution Time: 322m 24s 📊 Comparison with Base Branch
All tests are passing successfully. ✅ Patch coverage is 93.33%. Project has 2414 uncovered lines. Files with missing lines (1)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
+ Coverage 89.85% 89.85% —%
==========================================
Files 192 192 —
Lines 23745 23774 +29
Branches 8198 8206 +8
==========================================
+ Hits 21334 21360 +26
- Misses 2411 2414 +3
- Partials 1344 1348 +4Generated by Codecov Action |
wahajahmed010
left a comment
There was a problem hiding this comment.
Review: #6609 — fix(asyncpg): distinct span ops for cursor iteration/fetch
Logic: ✅ Clean approach. Using ContextVar flags to distinguish cursor iteration vs fetch vs regular query execution, then overriding sentry.op accordingly. The N+1 detector can then filter out db.cursor.iter/db.cursor.fetch spans.
Tests: ✅ Comprehensive — tests both streaming and static span modes, both cursor iteration and fetch paths. Asserts exact span count (5 for prefetch=5, 2 for fetch(10)+fetch(10)).
Edge case: What if someone uses CursorIterator.__anext__ directly without going through async for? The monkey-patch on CursorIterator.__anext__ covers that. ✅
Concern: The ContextVar flags are module-level — if two cursors are active concurrently (e.g., in different tasks), the flags could cross-contaminate. However, ContextVar is task-local by design in Python, so this should be safe. ✅
Performance: Minimal overhead — just a ContextVar set/get per call. ✅
Verdict: Well-structured fix. Ship it.
When asyncpg iterates a server-side cursor (via CursorIterator.anext) or fetches rows in batches (via Cursor.fetch), each batch triggers BaseCursor._bind_exec or BaseCursor._exec, producing a burst of identical spans. Sentry's N+1 detector incorrectly flags these as N+1 queries.
Set
sentry.optodb.cursor.iterfor CursorIterator-driven spans anddb.cursor.fetchfor Cursor.fetch-driven spans so the backend can exclude them from N+1 detection.Adds span_op_override_value parameter to record_sql_queries to propagate the override without changing existing callers.
This will require changes on the backend in order to not trigger n+1 alerts on these ops.
Fixes #6576
Fixes PY-2534