2121
2222import sentry_sdk
2323from sentry_sdk import capture_message , start_transaction
24- from sentry_sdk .consts import SPANDATA
24+ from sentry_sdk .consts import OP , SPANDATA
2525from sentry_sdk .integrations .asyncpg import AsyncPGIntegration
2626from sentry_sdk .tracing_utils import record_sql_queries
2727from tests .conftest import ApproxDict
@@ -1361,75 +1361,57 @@ async def test_query_source_prepare(
13611361
13621362@pytest .mark .asyncio
13631363@pytest .mark .parametrize ("span_streaming" , [True , False ])
1364- async def test_cursor__bind_exec_creates_spans (
1364+ async def test_cursor_iteration_creates_db_cursor_iter_spans (
13651365 sentry_init , capture_events , capture_items , span_streaming
13661366) -> None :
13671367 """
1368- Exercises the bind_exec patch through the iterator that's created in asyncpg when "for record in conn.cursor" is called.
1369- See https://github.com/MagicStack/asyncpg/blob/db8ecc2a38e16fb0c090aef6f5506547c2831c24/asyncpg/cursor.py#L234
1368+ Regression test for https://github.com/getsentry/sentry-python/issues/6576
1369+
1370+ When iterating a server-side cursor with a small prefetch, asyncpg fetches
1371+ rows in batches. Each batch triggers BaseCursor._bind_exec (on first query) and
1372+ BaseCursor._exec (second query onwards) through CursorIterator.__anext__, which creates a
1373+ span with the same query description. The resulting burst of identical spans
1374+ causes Sentry's N+1 query detector to raise a false positive.
1375+
1376+ To mitigate, we set the "op"/"sentry.op" to `db.cursor.iter` instead of `db`
1377+ so that the sentry backend can exclude these spans from n+1 detection.
13701378 """
13711379 sentry_init (
13721380 integrations = [AsyncPGIntegration ()],
13731381 traces_sample_rate = 1.0 ,
1374- enable_db_query_source = True ,
1375- db_query_source_threshold_ms = 0 ,
13761382 _experiments = {
13771383 "trace_lifecycle" : "stream" if span_streaming else "static" ,
13781384 },
13791385 )
13801386
13811387 if span_streaming :
13821388 items = capture_items ("span" )
1389+
13831390 with sentry_sdk .traces .start_span (name = "test_segment" ):
13841391 conn : Connection = await connect (PG_CONNECTION_URI )
13851392
13861393 await conn .executemany (
13871394 "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)" ,
1388- [
1389- ("Bob" , "secret_pw" , datetime .date (1984 , 3 , 1 )),
1390- ("Alice" , "pw" , datetime .date (1990 , 12 , 25 )),
1391- ],
1395+ [(f"user-{ i } " , "pw" , datetime .date (1990 , 1 , 1 )) for i in range (20 )],
13921396 )
13931397
13941398 async with conn .transaction ():
1395- async for record in conn .cursor (
1396- "SELECT * FROM users WHERE dob > $1" ,
1397- datetime .date (1970 , 1 , 1 ),
1398- ):
1399+ async for _record in conn .cursor ("SELECT * FROM users" , prefetch = 5 ):
13991400 pass
14001401
14011402 await conn .close ()
1402- sentry_sdk .flush ()
14031403
1404- spans = [item .payload for item in items ]
1405-
1406- assert len (spans ) == 6
1407-
1408- connect_span = spans [0 ]
1409- executemany_span = spans [1 ]
1410- begin_span = spans [2 ]
1411- bind_exec_span = spans [3 ]
1412- commit_span = spans [4 ]
1413- segment = spans [5 ]
1404+ sentry_sdk .flush ()
14141405
1415- assert connect_span ["name" ] == "connect"
1416- assert (
1417- executemany_span ["name" ]
1418- == "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)"
1419- )
1420- assert begin_span ["name" ] == "BEGIN;"
1421- assert bind_exec_span ["name" ] == "SELECT * FROM users WHERE dob > $1"
1422- assert commit_span ["name" ] == "COMMIT;"
1423- assert segment ["name" ] == "test_segment"
1406+ cursor_iter_spans = [
1407+ item .payload
1408+ for item in items
1409+ if item .payload .get ("name" ) == "SELECT * FROM users"
1410+ ]
14241411
1425- assert bind_exec_span ["attributes" ]["sentry.origin" ] == "auto.db.asyncpg"
1426- assert bind_exec_span ["attributes" ]["sentry.op" ] == "db"
1427- assert bind_exec_span ["attributes" ]["db.system.name" ] == "postgresql"
1428- assert bind_exec_span ["attributes" ]["db.driver.name" ] == "asyncpg"
1429- assert bind_exec_span ["attributes" ]["server.address" ] == PG_HOST
1430- assert bind_exec_span ["attributes" ]["server.port" ] == PG_PORT
1431- assert bind_exec_span ["attributes" ]["db.namespace" ] == PG_NAME
1432- assert bind_exec_span ["attributes" ]["db.user" ] == PG_USER
1412+ assert len (cursor_iter_spans ) == 5
1413+ for span in cursor_iter_spans :
1414+ assert span ["attributes" ]["sentry.op" ] == OP .DB_CURSOR_ITERATOR
14331415 else :
14341416 events = capture_events ()
14351417
@@ -1438,57 +1420,28 @@ async def test_cursor__bind_exec_creates_spans(
14381420
14391421 await conn .executemany (
14401422 "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)" ,
1441- [
1442- ("Bob" , "secret_pw" , datetime .date (1984 , 3 , 1 )),
1443- ("Alice" , "pw" , datetime .date (1990 , 12 , 25 )),
1444- ],
1423+ [(f"user-{ i } " , "pw" , datetime .date (1990 , 1 , 1 )) for i in range (20 )],
14451424 )
14461425
14471426 async with conn .transaction ():
1448- async for record in conn .cursor (
1449- "SELECT * FROM users WHERE dob > $1" ,
1450- datetime .date (1970 , 1 , 1 ),
1451- ):
1427+ async for _record in conn .cursor ("SELECT * FROM users" , prefetch = 5 ):
14521428 pass
14531429
14541430 await conn .close ()
14551431
14561432 (event ,) = events
14571433
1458- assert len (event ["spans" ]) == 5
1459-
1460- connect_span = event ["spans" ][0 ]
1461- executemany_span = event ["spans" ][1 ]
1462- begin_span = event ["spans" ][2 ]
1463- bind_exec_span = event ["spans" ][3 ]
1464- commit_span = event ["spans" ][4 ]
1434+ cursor_iter_spans = [
1435+ s for s in event ["spans" ] if s .get ("description" ) == "SELECT * FROM users"
1436+ ]
14651437
1466- assert connect_span ["description" ] == "connect"
1467- assert (
1468- executemany_span ["description" ]
1469- == "INSERT INTO users(name, password, dob) VALUES($1, $2, $3)"
1470- )
1471- assert begin_span ["description" ] == "BEGIN;"
1472- assert bind_exec_span ["description" ] == "SELECT * FROM users WHERE dob > $1"
1473- assert commit_span ["description" ] == "COMMIT;"
1474-
1475- assert bind_exec_span ["origin" ] == "auto.db.asyncpg"
1476- assert bind_exec_span ["data" ]["db.system" ] == "postgresql"
1477- assert bind_exec_span ["data" ]["db.driver.name" ] == "asyncpg"
1478- assert bind_exec_span ["data" ]["server.address" ] == PG_HOST
1479- assert bind_exec_span ["data" ]["server.port" ] == PG_PORT
1480- assert bind_exec_span ["data" ]["db.name" ] == PG_NAME
1481- assert bind_exec_span ["data" ]["db.user" ] == PG_USER
1482-
1483- _assert_query_source (
1484- bind_exec_span ,
1485- span_streaming ,
1486- "test_cursor__bind_exec_creates_spans" ,
1487- )
1438+ assert len (cursor_iter_spans ) == 5
1439+ for span in cursor_iter_spans :
1440+ assert span ["op" ] == OP .DB_CURSOR_ITERATOR
14881441
14891442
14901443@pytest .mark .asyncio
1491- async def test_cursor__exec_methods_create_spans (sentry_init , capture_events ) -> None :
1444+ async def test_cursor_fetch_methods_create_spans (sentry_init , capture_events ) -> None :
14921445 sentry_init (
14931446 integrations = [AsyncPGIntegration ()],
14941447 traces_sample_rate = 1.0 ,
@@ -1543,9 +1496,10 @@ async def test_cursor__exec_methods_create_spans(sentry_init, capture_events) ->
15431496 assert span ["data" ]["db.cursor" ] is not None
15441497 assert span ["data" ]["db.system" ] == "postgresql"
15451498 assert span ["data" ]["db.driver.name" ] == "asyncpg"
1499+ assert span ["op" ] == OP .DB_CURSOR_FETCH
15461500 assert span ["origin" ] == "auto.db.asyncpg"
15471501 _assert_query_source (
15481502 span ,
15491503 False ,
1550- "test_cursor__exec_methods_create_spans " ,
1504+ "test_cursor_fetch_methods_create_spans " ,
15511505 )
0 commit comments