Skip to content

Commit 2eeb8c5

Browse files
authored
fix(query-source): Fix query source relative filepath (#2717)
When generating the filename attribute for stack trace frames, the SDK uses the `filename_for_module` function. When generating the `code.filepath` attribute for query spans, the SDK does not use that function. Because of this inconsistency, code mappings that work with stack frames sometimes don't work with queries that come from the same files. This change makes sure that query sources use `filename_for_module`, so the paths are consistent.
1 parent e07c0ac commit 2eeb8c5

File tree

15 files changed

+223
-1
lines changed

15 files changed

+223
-1
lines changed

sentry_sdk/tracing_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from sentry_sdk.consts import OP, SPANDATA
88
from sentry_sdk.utils import (
99
capture_internal_exceptions,
10+
filename_for_module,
1011
Dsn,
1112
match_regex_list,
1213
to_string,
@@ -255,7 +256,9 @@ def add_query_source(hub, span):
255256
except Exception:
256257
filepath = None
257258
if filepath is not None:
258-
if project_root is not None and filepath.startswith(project_root):
259+
if namespace is not None and not PY2:
260+
in_app_path = filename_for_module(namespace, filepath)
261+
elif project_root is not None and filepath.startswith(project_root):
259262
in_app_path = filepath.replace(project_root, "").lstrip(os.sep)
260263
else:
261264
in_app_path = filepath
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
import os
2+
import sys
13
import pytest
24

35
pytest.importorskip("asyncpg")
46
pytest.importorskip("pytest_asyncio")
7+
8+
# Load `asyncpg_helpers` into the module search path to test query source path names relative to module. See
9+
# `test_query_source_with_module_in_search_path`
10+
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))

tests/integrations/asyncpg/asyncpg_helpers/__init__.py

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
async def execute_query_in_connection(query, connection):
2+
await connection.execute(query)

tests/integrations/asyncpg/test_asyncpg.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
PG_PORT = 5432
2020

2121

22+
from sentry_sdk._compat import PY2
2223
import datetime
2324

2425
import asyncpg
@@ -592,6 +593,56 @@ async def test_query_source(sentry_init, capture_events):
592593
assert data.get(SPANDATA.CODE_FUNCTION) == "test_query_source"
593594

594595

596+
@pytest.mark.asyncio
597+
async def test_query_source_with_module_in_search_path(sentry_init, capture_events):
598+
"""
599+
Test that query source is relative to the path of the module it ran in
600+
"""
601+
sentry_init(
602+
integrations=[AsyncPGIntegration()],
603+
enable_tracing=True,
604+
enable_db_query_source=True,
605+
db_query_source_threshold_ms=0,
606+
)
607+
608+
events = capture_events()
609+
610+
from asyncpg_helpers.helpers import execute_query_in_connection
611+
612+
with start_transaction(name="test_transaction", sampled=True):
613+
conn: Connection = await connect(PG_CONNECTION_URI)
614+
615+
await execute_query_in_connection(
616+
"INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')",
617+
conn,
618+
)
619+
620+
await conn.close()
621+
622+
(event,) = events
623+
624+
span = event["spans"][-1]
625+
assert span["description"].startswith("INSERT INTO")
626+
627+
data = span.get("data", {})
628+
629+
assert SPANDATA.CODE_LINENO in data
630+
assert SPANDATA.CODE_NAMESPACE in data
631+
assert SPANDATA.CODE_FILEPATH in data
632+
assert SPANDATA.CODE_FUNCTION in data
633+
634+
assert type(data.get(SPANDATA.CODE_LINENO)) == int
635+
assert data.get(SPANDATA.CODE_LINENO) > 0
636+
if not PY2:
637+
assert data.get(SPANDATA.CODE_NAMESPACE) == "asyncpg_helpers.helpers"
638+
assert data.get(SPANDATA.CODE_FILEPATH) == "asyncpg_helpers/helpers.py"
639+
640+
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
641+
assert is_relative_path
642+
643+
assert data.get(SPANDATA.CODE_FUNCTION) == "execute_query_in_connection"
644+
645+
595646
@pytest.mark.asyncio
596647
async def test_no_query_source_if_duration_too_short(sentry_init, capture_events):
597648
sentry_init(

tests/integrations/django/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
import os
2+
import sys
13
import pytest
24

35
pytest.importorskip("django")
6+
7+
# Load `django_helpers` into the module search path to test query source path names relative to module. See
8+
# `test_query_source_with_module_in_search_path`
9+
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))

tests/integrations/django/django_helpers/__init__.py

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.contrib.auth.models import User
2+
from django.http import HttpResponse
3+
from django.views.decorators.csrf import csrf_exempt
4+
5+
6+
@csrf_exempt
7+
def postgres_select_orm(request, *args, **kwargs):
8+
user = User.objects.using("postgres").all().first()
9+
return HttpResponse("ok {}".format(user))

tests/integrations/django/myapp/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def path(path, *args, **kwargs):
2626

2727

2828
from . import views
29+
from django_helpers import views as helper_views
2930

3031
urlpatterns = [
3132
path("view-exc", views.view_exc, name="view_exc"),
@@ -59,6 +60,11 @@ def path(path, *args, **kwargs):
5960
path("template-test3", views.template_test3, name="template_test3"),
6061
path("postgres-select", views.postgres_select, name="postgres_select"),
6162
path("postgres-select-slow", views.postgres_select_orm, name="postgres_select_orm"),
63+
path(
64+
"postgres-select-slow-from-supplement",
65+
helper_views.postgres_select_orm,
66+
name="postgres_select_slow_from_supplement",
67+
),
6268
path(
6369
"permission-denied-exc",
6470
views.permission_denied_exc,

tests/integrations/django/test_db_query_data.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import pytest
55
from datetime import datetime
66

7+
from sentry_sdk._compat import PY2
78
from django import VERSION as DJANGO_VERSION
89
from django.db import connections
910

@@ -168,6 +169,62 @@ def test_query_source(sentry_init, client, capture_events):
168169
raise AssertionError("No db span found")
169170

170171

172+
@pytest.mark.forked
173+
@pytest_mark_django_db_decorator(transaction=True)
174+
def test_query_source_with_module_in_search_path(sentry_init, client, capture_events):
175+
"""
176+
Test that query source is relative to the path of the module it ran in
177+
"""
178+
client = Client(application)
179+
180+
sentry_init(
181+
integrations=[DjangoIntegration()],
182+
send_default_pii=True,
183+
traces_sample_rate=1.0,
184+
enable_db_query_source=True,
185+
db_query_source_threshold_ms=0,
186+
)
187+
188+
if "postgres" not in connections:
189+
pytest.skip("postgres tests disabled")
190+
191+
# trigger Django to open a new connection by marking the existing one as None.
192+
connections["postgres"].connection = None
193+
194+
events = capture_events()
195+
196+
_, status, _ = unpack_werkzeug_response(
197+
client.get(reverse("postgres_select_slow_from_supplement"))
198+
)
199+
assert status == "200 OK"
200+
201+
(event,) = events
202+
for span in event["spans"]:
203+
if span.get("op") == "db" and "auth_user" in span.get("description"):
204+
data = span.get("data", {})
205+
206+
assert SPANDATA.CODE_LINENO in data
207+
assert SPANDATA.CODE_NAMESPACE in data
208+
assert SPANDATA.CODE_FILEPATH in data
209+
assert SPANDATA.CODE_FUNCTION in data
210+
211+
assert type(data.get(SPANDATA.CODE_LINENO)) == int
212+
assert data.get(SPANDATA.CODE_LINENO) > 0
213+
214+
if not PY2:
215+
assert data.get(SPANDATA.CODE_NAMESPACE) == "django_helpers.views"
216+
assert data.get(SPANDATA.CODE_FILEPATH) == "django_helpers/views.py"
217+
218+
is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep
219+
assert is_relative_path
220+
221+
assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm"
222+
223+
break
224+
else:
225+
raise AssertionError("No db span found")
226+
227+
171228
@pytest.mark.forked
172229
@pytest_mark_django_db_decorator(transaction=True)
173230
def test_query_source_with_in_app_exclude(sentry_init, client, capture_events):

0 commit comments

Comments
 (0)