Skip to content

Commit 113d12d

Browse files
committed
test(tracing): Test add_query_source with modules outside of project root
When packages added in `in_app_include` are installed to a location outside of the project root directory, span from those packages are not extended with OTel compatible source code information. Cases include running Python from virtualenv created outside of the project root directory or Python packages installed into the system using package managers. This results in an inconsistency: spans from the same project are be different, depending on the deployment method. The change extends `test_query_source_with_in_app_include` to test the simulation of Django installed outside of the project root. The steps to manually reproduce the issue are as follows (case: a virtual environment created outside of the project root): ```bash docker run --replace --rm --detach \ --name sentry-postgres \ --env POSTGRES_USER=sentry \ --env POSTGRES_PASSWORD=sentry \ --publish 5432:5432 \ postgres distrobox create \ --image ubuntu:24.04 \ --name sentry-test-in_app_include-venv distrobox enter sentry-test-in_app_include-venv python3 -m venv /tmp/.venv-test-in_app_include source /tmp/.venv-test-in_app_include/bin/activate pip install \ -r requirements-devenv.txt \ pytest-django \ psycopg2-binary \ -e .[django] export SENTRY_PYTHON_TEST_POSTGRES_USER=sentry export SENTRY_PYTHON_TEST_POSTGRES_PASSWORD=sentry pytest tests/integrations/django/test_db_query_data.py \ -k test_query_source_with_in_app_include # FAIL ``` The steps to manually reproduce the issue are as follows (case: Django is installed through system packages): ```bash docker run --replace --rm --detach \ --name sentry-postgres \ --env POSTGRES_USER=sentry \ --env POSTGRES_PASSWORD=sentry \ --publish 5432:5432 \ postgres distrobox create \ --image ubuntu:24.04 \ --name sentry-test-in_app_include-os distrobox enter sentry-test-in_app_include-os sudo apt install \ python3-django python3-pytest python3-pytest-cov \ python3-pytest-django python3-jsonschema python3-urllib3 \ python3-certifi python3-werkzeug python3-psycopg2 export SENTRY_PYTHON_TEST_POSTGRES_USER=sentry export SENTRY_PYTHON_TEST_POSTGRES_PASSWORD=sentry pytest tests/integrations/django/test_db_query_data.py \ -k test_query_source_with_in_app_include # FAIL ```
1 parent fc5db4f commit 113d12d

File tree

3 files changed

+66
-14
lines changed

3 files changed

+66
-14
lines changed

sentry_sdk/tracing_utils.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,14 @@ def maybe_create_breadcrumbs_from_span(scope, span):
170170
)
171171

172172

173+
def _get_frame_module_abs_path(frame):
174+
# type: (FrameType) -> Optional[str]
175+
try:
176+
return frame.f_code.co_filename
177+
except Exception:
178+
return None
179+
180+
173181
def add_query_source(span):
174182
# type: (sentry_sdk.tracing.Span) -> None
175183
"""
@@ -200,10 +208,7 @@ def add_query_source(span):
200208
# Find the correct frame
201209
frame = sys._getframe() # type: Union[FrameType, None]
202210
while frame is not None:
203-
try:
204-
abs_path = frame.f_code.co_filename
205-
except Exception:
206-
abs_path = ""
211+
abs_path = _get_frame_module_abs_path(frame)
207212

208213
try:
209214
namespace = frame.f_globals.get("__name__") # type: Optional[str]
@@ -224,7 +229,8 @@ def add_query_source(span):
224229
should_be_included = True
225230

226231
if (
227-
abs_path.startswith(project_root)
232+
abs_path is not None
233+
and abs_path.startswith(project_root)
228234
and should_be_included
229235
and not is_sentry_sdk_frame
230236
):
@@ -250,10 +256,7 @@ def add_query_source(span):
250256
if namespace is not None:
251257
span.set_data(SPANDATA.CODE_NAMESPACE, namespace)
252258

253-
try:
254-
filepath = frame.f_code.co_filename
255-
except Exception:
256-
filepath = None
259+
filepath = _get_frame_module_abs_path(frame)
257260
if filepath is not None:
258261
if namespace is not None:
259262
in_app_path = filename_for_module(namespace, filepath)

sentry_sdk/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1062,8 +1062,11 @@ def _module_in_list(name, items):
10621062

10631063

10641064
def _is_external_source(abs_path):
1065-
# type: (str) -> bool
1065+
# type: (Optional[str]) -> bool
10661066
# check if frame is in 'site-packages' or 'dist-packages'
1067+
if abs_path is None:
1068+
return False
1069+
10671070
external_source = (
10681071
re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None
10691072
)

tests/integrations/django/test_db_query_data.py

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import contextlib
12
import os
23

34
import pytest
45
from datetime import datetime
6+
from pathlib import Path, PurePosixPath, PureWindowsPath
57
from unittest import mock
68

79
from django import VERSION as DJANGO_VERSION
@@ -15,14 +17,20 @@
1517
from werkzeug.test import Client
1618

1719
from sentry_sdk import start_transaction
20+
from sentry_sdk._types import TYPE_CHECKING
1821
from sentry_sdk.consts import SPANDATA
1922
from sentry_sdk.integrations.django import DjangoIntegration
20-
from sentry_sdk.tracing_utils import record_sql_queries
23+
from sentry_sdk.tracing_utils import _get_frame_module_abs_path, record_sql_queries
24+
from sentry_sdk.utils import _module_in_list
2125

2226
from tests.conftest import unpack_werkzeug_response
2327
from tests.integrations.django.utils import pytest_mark_django_db_decorator
2428
from tests.integrations.django.myapp.wsgi import application
2529

30+
if TYPE_CHECKING:
31+
from types import FrameType
32+
from typing import Optional
33+
2634

2735
@pytest.fixture
2836
def client():
@@ -283,7 +291,10 @@ def test_query_source_with_in_app_exclude(sentry_init, client, capture_events):
283291

284292
@pytest.mark.forked
285293
@pytest_mark_django_db_decorator(transaction=True)
286-
def test_query_source_with_in_app_include(sentry_init, client, capture_events):
294+
@pytest.mark.parametrize("django_outside_of_project_root", [False, True])
295+
def test_query_source_with_in_app_include(
296+
sentry_init, client, capture_events, django_outside_of_project_root
297+
):
287298
sentry_init(
288299
integrations=[DjangoIntegration()],
289300
send_default_pii=True,
@@ -301,8 +312,43 @@ def test_query_source_with_in_app_include(sentry_init, client, capture_events):
301312

302313
events = capture_events()
303314

304-
_, status, _ = unpack_werkzeug_response(client.get(reverse("postgres_select_orm")))
305-
assert status == "200 OK"
315+
# Simulate Django installation outside of the project root
316+
original_get_frame_module_abs_path = _get_frame_module_abs_path
317+
318+
def patched_get_frame_module_abs_path_function(frame):
319+
# type: (FrameType) -> Optional[str]
320+
result = original_get_frame_module_abs_path(frame)
321+
if result is None:
322+
return result
323+
324+
namespace = frame.f_globals.get("__name__")
325+
if _module_in_list(namespace, ["django"]):
326+
# Since the result is used as `str` only and not accessed,
327+
# it is sufficient to generate non-existent path
328+
# that would be located outside of the project root.
329+
# Use UNC path for simplicity on Windows.
330+
non_existent_prefix = (
331+
PureWindowsPath("//outside-of-project-root")
332+
if os.name == "nt"
333+
else PurePosixPath("/outside-of-project-root")
334+
)
335+
result = str(non_existent_prefix.joinpath(*Path(result).parts[1:]))
336+
return result
337+
338+
patched_get_frame_module_abs_path = (
339+
mock.patch(
340+
"sentry_sdk.tracing_utils._get_frame_module_abs_path",
341+
patched_get_frame_module_abs_path_function,
342+
)
343+
if django_outside_of_project_root
344+
else contextlib.suppress()
345+
)
346+
347+
with patched_get_frame_module_abs_path:
348+
_, status, _ = unpack_werkzeug_response(
349+
client.get(reverse("postgres_select_orm"))
350+
)
351+
assert status == "200 OK"
306352

307353
(event,) = events
308354
for span in event["spans"]:

0 commit comments

Comments
 (0)