Skip to content

Commit cb6ed8c

Browse files
ericapisaniclaude
andauthored
ref(asyncpg): Normalize query whitespace in integration (#5855)
Normalize multiline SQL query whitespace in the asyncpg integration so that span descriptions contain single-line queries with collapsed whitespace. asyncpg passes raw multiline SQL strings as span descriptions. This makes it difficult for users to match queries in `before_send_transaction` callbacks — they'd need to account for newlines and varying indentation instead of writing simple substring checks like `"SELECT id, name FROM users" in desc`. Fixes PY-2255 and #5850 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a5d04d6 commit cb6ed8c

File tree

2 files changed

+85
-9
lines changed

2 files changed

+85
-9
lines changed

sentry_sdk/integrations/asyncpg.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
import contextlib
3+
import re
34
from typing import Any, TypeVar, Callable, Awaitable, Iterator
45

56
import sentry_sdk
@@ -55,6 +56,10 @@ def setup_once() -> None:
5556
T = TypeVar("T")
5657

5758

59+
def _normalize_query(query: str) -> str:
60+
return re.sub(r"\s+", " ", query).strip()
61+
62+
5863
def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]":
5964
async def _inner(*args: "Any", **kwargs: "Any") -> "T":
6065
if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None:
@@ -67,7 +72,7 @@ async def _inner(*args: "Any", **kwargs: "Any") -> "T":
6772
if len(args) > 2:
6873
return await f(*args, **kwargs)
6974

70-
query = args[1]
75+
query = _normalize_query(args[1])
7176
with record_sql_queries(
7277
cursor=None,
7378
query=query,
@@ -103,6 +108,7 @@ def _record(
103108

104109
param_style = "pyformat" if params_list else None
105110

111+
query = _normalize_query(query)
106112
with record_sql_queries(
107113
cursor=cursor,
108114
query=query,

tests/integrations/asyncpg/test_asyncpg.py

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -463,10 +463,7 @@ async def test_connection_pool(sentry_init, capture_events) -> None:
463463
{
464464
"category": "query",
465465
"data": {},
466-
"message": "SELECT pg_advisory_unlock_all();\n"
467-
"CLOSE ALL;\n"
468-
"UNLISTEN *;\n"
469-
"RESET ALL;",
466+
"message": "SELECT pg_advisory_unlock_all(); CLOSE ALL; UNLISTEN *; RESET ALL;",
470467
"type": "default",
471468
},
472469
{
@@ -478,10 +475,7 @@ async def test_connection_pool(sentry_init, capture_events) -> None:
478475
{
479476
"category": "query",
480477
"data": {},
481-
"message": "SELECT pg_advisory_unlock_all();\n"
482-
"CLOSE ALL;\n"
483-
"UNLISTEN *;\n"
484-
"RESET ALL;",
478+
"message": "SELECT pg_advisory_unlock_all(); CLOSE ALL; UNLISTEN *; RESET ALL;",
485479
"type": "default",
486480
},
487481
]
@@ -786,3 +780,79 @@ async def test_span_origin(sentry_init, capture_events):
786780

787781
for span in event["spans"]:
788782
assert span["origin"] == "auto.db.asyncpg"
783+
784+
785+
@pytest.mark.asyncio
786+
async def test_multiline_query_description_normalized(sentry_init, capture_events):
787+
sentry_init(
788+
integrations=[AsyncPGIntegration()],
789+
traces_sample_rate=1.0,
790+
)
791+
events = capture_events()
792+
793+
with start_transaction(name="test_transaction"):
794+
conn: Connection = await connect(PG_CONNECTION_URI)
795+
await conn.execute(
796+
"""
797+
SELECT
798+
id,
799+
name
800+
FROM
801+
users
802+
WHERE
803+
name = 'Alice'
804+
"""
805+
)
806+
await conn.close()
807+
808+
(event,) = events
809+
810+
spans = [
811+
s
812+
for s in event["spans"]
813+
if s["op"] == "db" and "SELECT" in s.get("description", "")
814+
]
815+
assert len(spans) == 1
816+
assert spans[0]["description"] == "SELECT id, name FROM users WHERE name = 'Alice'"
817+
818+
819+
@pytest.mark.asyncio
820+
async def test_before_send_transaction_sees_normalized_description(
821+
sentry_init, capture_events
822+
):
823+
def before_send_transaction(event, hint):
824+
for span in event.get("spans", []):
825+
desc = span.get("description", "")
826+
if "SELECT id, name FROM users" in desc:
827+
span["description"] = "filtered"
828+
return event
829+
830+
sentry_init(
831+
integrations=[AsyncPGIntegration()],
832+
traces_sample_rate=1.0,
833+
before_send_transaction=before_send_transaction,
834+
)
835+
events = capture_events()
836+
837+
with start_transaction(name="test_transaction"):
838+
conn: Connection = await connect(PG_CONNECTION_URI)
839+
await conn.execute(
840+
"""
841+
SELECT
842+
id,
843+
name
844+
FROM
845+
users
846+
"""
847+
)
848+
await conn.close()
849+
850+
(event,) = events
851+
spans = [
852+
s
853+
for s in event["spans"]
854+
if s["op"] == "db" and "filtered" in s.get("description", "")
855+
]
856+
857+
assert len(spans) == 1
858+
assert spans[0]["description"] == "filtered"

0 commit comments

Comments
 (0)