Skip to content

Commit eaf165e

Browse files
ericapisaniclaude
andcommitted
ref(asyncpg): Move query whitespace normalization into integration
Instead of normalizing span descriptions generically in client.py before calling before_send_transaction, normalize queries at the source in the asyncpg integration. This is more correct because the normalization is specific to SQL query formatting, not a general concern for all span descriptions. Remove _clean_span_descriptions from _Client and the generic tests. Add asyncpg-specific tests for normalized descriptions and before_send_transaction interaction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e804242 commit eaf165e

4 files changed

Lines changed: 86 additions & 96 deletions

File tree

sentry_sdk/client.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from importlib import import_module
88
from typing import TYPE_CHECKING, List, Dict, cast, overload
99
import warnings
10-
import re
10+
1111
from sentry_sdk._compat import check_uwsgi_thread_support
1212
from sentry_sdk._metrics_batcher import MetricsBatcher
1313
from sentry_sdk._span_batcher import SpanBatcher
@@ -688,7 +688,6 @@ def _prepare_event(
688688
):
689689
new_event = None
690690
spans_before = len(cast(List[Dict[str, object]], event.get("spans", [])))
691-
self._clean_span_descriptions(event)
692691
with capture_internal_exceptions():
693692
new_event = before_send_transaction(event, hint or {})
694693
if new_event is None:
@@ -713,12 +712,6 @@ def _prepare_event(
713712

714713
return event
715714

716-
def _clean_span_descriptions(self, event: "Event") -> None:
717-
for s in event.get("spans", []):
718-
if "description" in s:
719-
cleaned_description = re.sub(r"\s+", " ", s["description"]).strip()
720-
s["description"] = cleaned_description
721-
722715
def _is_ignored_error(self, event: "Event", hint: "Hint") -> bool:
723716
exc_info = hint.get("exc_info")
724717
if exc_info is None:

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"

tests/test_basics.py

Lines changed: 0 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -185,85 +185,6 @@ def before_send_transaction(event, hint):
185185
assert event["extra"] == {"before_send_transaction_called": True}
186186

187187

188-
def test_before_send_transaction_span_description_contains_newlines(
189-
sentry_init, capture_events
190-
):
191-
def before_send_transaction(event, hint):
192-
for span in event.get("spans", []):
193-
if (
194-
span.get("description", "")
195-
== "SELECT u.id, u.name, u.email FROM users;"
196-
):
197-
span["description"] = "filtered"
198-
return event
199-
200-
sentry_init(
201-
before_send_transaction=before_send_transaction,
202-
traces_sample_rate=1.0,
203-
)
204-
events = capture_events()
205-
206-
description = "SELECT u.id,\n u.name,\n u.email\n FROM users;"
207-
208-
with start_transaction(name="test_transaction"):
209-
with sentry_sdk.start_span(op="db", description=description):
210-
pass
211-
212-
(event,) = events
213-
assert event["transaction"] == "test_transaction"
214-
215-
spans = event["spans"]
216-
assert len(spans) == 1
217-
assert spans[0]["description"] == "filtered"
218-
assert spans[0]["op"] == "db"
219-
220-
221-
def test_before_send_transaction_span_description_contains_multiple_lines(
222-
sentry_init, capture_events
223-
):
224-
def before_send_transaction(event, hint):
225-
for span in event.get("spans", []):
226-
if (
227-
span.get("description", "")
228-
== "SELECT u.id, u.name, u.email, p.title AS post_title, p.created_at AS post_date FROM users u JOIN posts p ON u.id = p.user_id WHERE u.active = true ORDER BY p.created_at DESC"
229-
):
230-
span["description"] = "no bueno"
231-
return event
232-
233-
sentry_init(
234-
before_send_transaction=before_send_transaction,
235-
traces_sample_rate=1.0,
236-
)
237-
events = capture_events()
238-
239-
description = """SELECT
240-
u.id,
241-
u.name,
242-
u.email,
243-
p.title AS post_title,
244-
p.created_at AS post_date
245-
FROM
246-
users u
247-
JOIN
248-
posts p ON u.id = p.user_id
249-
WHERE
250-
u.active = true
251-
ORDER BY
252-
p.created_at DESC"""
253-
254-
with start_transaction(name="test_transaction"):
255-
with sentry_sdk.start_span(op="db", description=description):
256-
pass
257-
258-
(event,) = events
259-
assert event["transaction"] == "test_transaction"
260-
261-
spans = event["spans"]
262-
assert len(spans) == 1
263-
assert spans[0]["description"] == "no bueno"
264-
assert spans[0]["op"] == "db"
265-
266-
267188
def test_option_before_send_transaction_discard(sentry_init, capture_events):
268189
def before_send_transaction_discard(event, hint):
269190
return None

0 commit comments

Comments
 (0)