diff --git a/sentry_sdk/integrations/asyncpg.py b/sentry_sdk/integrations/asyncpg.py index 7f3591154a..d1cf6fcc92 100644 --- a/sentry_sdk/integrations/asyncpg.py +++ b/sentry_sdk/integrations/asyncpg.py @@ -1,5 +1,6 @@ from __future__ import annotations import contextlib +import re from typing import Any, TypeVar, Callable, Awaitable, Iterator import sentry_sdk @@ -55,6 +56,10 @@ def setup_once() -> None: T = TypeVar("T") +def _normalize_query(query: str) -> str: + return re.sub(r"\s+", " ", query).strip() + + def _wrap_execute(f: "Callable[..., Awaitable[T]]") -> "Callable[..., Awaitable[T]]": async def _inner(*args: "Any", **kwargs: "Any") -> "T": if sentry_sdk.get_client().get_integration(AsyncPGIntegration) is None: @@ -67,7 +72,7 @@ async def _inner(*args: "Any", **kwargs: "Any") -> "T": if len(args) > 2: return await f(*args, **kwargs) - query = args[1] + query = _normalize_query(args[1]) with record_sql_queries( cursor=None, query=query, @@ -103,6 +108,7 @@ def _record( param_style = "pyformat" if params_list else None + query = _normalize_query(query) with record_sql_queries( cursor=cursor, query=query, diff --git a/tests/integrations/asyncpg/test_asyncpg.py b/tests/integrations/asyncpg/test_asyncpg.py index e23612c055..62b5f166f7 100644 --- a/tests/integrations/asyncpg/test_asyncpg.py +++ b/tests/integrations/asyncpg/test_asyncpg.py @@ -463,10 +463,7 @@ async def test_connection_pool(sentry_init, capture_events) -> None: { "category": "query", "data": {}, - "message": "SELECT pg_advisory_unlock_all();\n" - "CLOSE ALL;\n" - "UNLISTEN *;\n" - "RESET ALL;", + "message": "SELECT pg_advisory_unlock_all(); CLOSE ALL; UNLISTEN *; RESET ALL;", "type": "default", }, { @@ -478,10 +475,7 @@ async def test_connection_pool(sentry_init, capture_events) -> None: { "category": "query", "data": {}, - "message": "SELECT pg_advisory_unlock_all();\n" - "CLOSE ALL;\n" - "UNLISTEN *;\n" - "RESET ALL;", + "message": "SELECT pg_advisory_unlock_all(); CLOSE ALL; UNLISTEN *; RESET ALL;", "type": "default", }, ] @@ -786,3 +780,79 @@ async def test_span_origin(sentry_init, capture_events): for span in event["spans"]: assert span["origin"] == "auto.db.asyncpg" + + +@pytest.mark.asyncio +async def test_multiline_query_description_normalized(sentry_init, capture_events): + sentry_init( + integrations=[AsyncPGIntegration()], + traces_sample_rate=1.0, + ) + events = capture_events() + + with start_transaction(name="test_transaction"): + conn: Connection = await connect(PG_CONNECTION_URI) + await conn.execute( + """ + SELECT + id, + name + FROM + users + WHERE + name = 'Alice' + """ + ) + await conn.close() + + (event,) = events + + spans = [ + s + for s in event["spans"] + if s["op"] == "db" and "SELECT" in s.get("description", "") + ] + assert len(spans) == 1 + assert spans[0]["description"] == "SELECT id, name FROM users WHERE name = 'Alice'" + + +@pytest.mark.asyncio +async def test_before_send_transaction_sees_normalized_description( + sentry_init, capture_events +): + def before_send_transaction(event, hint): + for span in event.get("spans", []): + desc = span.get("description", "") + if "SELECT id, name FROM users" in desc: + span["description"] = "filtered" + return event + + sentry_init( + integrations=[AsyncPGIntegration()], + traces_sample_rate=1.0, + before_send_transaction=before_send_transaction, + ) + events = capture_events() + + with start_transaction(name="test_transaction"): + conn: Connection = await connect(PG_CONNECTION_URI) + await conn.execute( + """ + SELECT + id, + name + FROM + users + """ + ) + await conn.close() + + (event,) = events + spans = [ + s + for s in event["spans"] + if s["op"] == "db" and "filtered" in s.get("description", "") + ] + + assert len(spans) == 1 + assert spans[0]["description"] == "filtered"