
Enabling integration tests with a PostgreSQL server led to the discovery of the issue, which is documented at https://github.com/getsentry/sentry-python/issues/3312. Short version: Previously, when packages added in `in_app_include` were installed to a location outside of the project root directory, span from those packages were 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 resulted in an inconsistency: spans from the same project would be different, depending on the deployment method. Add a patch to fix this issue as part of the update. [skip changelog]
238 lines
8.5 KiB
Diff
238 lines
8.5 KiB
Diff
From da164dad9eea765f2156cc44ef9a86a848aff4d9 Mon Sep 17 00:00:00 2001
|
|
From: Roman Inflianskas <rominf@pm.me>
|
|
Date: Thu, 18 Jul 2024 18:54:14 +0300
|
|
Subject: [PATCH 1/2] 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
|
|
```
|
|
---
|
|
sentry_sdk/tracing_utils.py | 21 ++++----
|
|
sentry_sdk/utils.py | 5 +-
|
|
.../integrations/django/test_db_query_data.py | 54 +++++++++++++++++--
|
|
3 files changed, 66 insertions(+), 14 deletions(-)
|
|
|
|
diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py
|
|
index ba20dc84..47e14ecd 100644
|
|
--- a/sentry_sdk/tracing_utils.py
|
|
+++ b/sentry_sdk/tracing_utils.py
|
|
@@ -170,6 +170,14 @@ def maybe_create_breadcrumbs_from_span(scope, span):
|
|
)
|
|
|
|
|
|
+def _get_frame_module_abs_path(frame):
|
|
+ # type: (FrameType) -> Optional[str]
|
|
+ try:
|
|
+ return frame.f_code.co_filename
|
|
+ except Exception:
|
|
+ return None
|
|
+
|
|
+
|
|
def add_query_source(span):
|
|
# type: (sentry_sdk.tracing.Span) -> None
|
|
"""
|
|
@@ -200,10 +208,7 @@ def add_query_source(span):
|
|
# Find the correct frame
|
|
frame = sys._getframe() # type: Union[FrameType, None]
|
|
while frame is not None:
|
|
- try:
|
|
- abs_path = frame.f_code.co_filename
|
|
- except Exception:
|
|
- abs_path = ""
|
|
+ abs_path = _get_frame_module_abs_path(frame)
|
|
|
|
try:
|
|
namespace = frame.f_globals.get("__name__") # type: Optional[str]
|
|
@@ -224,7 +229,8 @@ def add_query_source(span):
|
|
should_be_included = True
|
|
|
|
if (
|
|
- abs_path.startswith(project_root)
|
|
+ abs_path is not None
|
|
+ and abs_path.startswith(project_root)
|
|
and should_be_included
|
|
and not is_sentry_sdk_frame
|
|
):
|
|
@@ -250,10 +256,7 @@ def add_query_source(span):
|
|
if namespace is not None:
|
|
span.set_data(SPANDATA.CODE_NAMESPACE, namespace)
|
|
|
|
- try:
|
|
- filepath = frame.f_code.co_filename
|
|
- except Exception:
|
|
- filepath = None
|
|
+ filepath = _get_frame_module_abs_path(frame)
|
|
if filepath is not None:
|
|
if namespace is not None:
|
|
in_app_path = filename_for_module(namespace, filepath)
|
|
diff --git a/sentry_sdk/utils.py b/sentry_sdk/utils.py
|
|
index 8a805d3d..fcbff45e 100644
|
|
--- a/sentry_sdk/utils.py
|
|
+++ b/sentry_sdk/utils.py
|
|
@@ -1058,8 +1058,11 @@ def _module_in_list(name, items):
|
|
|
|
|
|
def _is_external_source(abs_path):
|
|
- # type: (str) -> bool
|
|
+ # type: (Optional[str]) -> bool
|
|
# check if frame is in 'site-packages' or 'dist-packages'
|
|
+ if abs_path is None:
|
|
+ return False
|
|
+
|
|
external_source = (
|
|
re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None
|
|
)
|
|
diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py
|
|
index 087fc5ad..265c6a79 100644
|
|
--- a/tests/integrations/django/test_db_query_data.py
|
|
+++ b/tests/integrations/django/test_db_query_data.py
|
|
@@ -1,7 +1,9 @@
|
|
+import contextlib
|
|
import os
|
|
|
|
import pytest
|
|
from datetime import datetime
|
|
+from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
from unittest import mock
|
|
|
|
from django import VERSION as DJANGO_VERSION
|
|
@@ -15,14 +17,20 @@ except ImportError:
|
|
from werkzeug.test import Client
|
|
|
|
from sentry_sdk import start_transaction
|
|
+from sentry_sdk._types import TYPE_CHECKING
|
|
from sentry_sdk.consts import SPANDATA
|
|
from sentry_sdk.integrations.django import DjangoIntegration
|
|
-from sentry_sdk.tracing_utils import record_sql_queries
|
|
+from sentry_sdk.tracing_utils import _get_frame_module_abs_path, record_sql_queries
|
|
+from sentry_sdk.utils import _module_in_list
|
|
|
|
from tests.conftest import unpack_werkzeug_response
|
|
from tests.integrations.django.utils import pytest_mark_django_db_decorator
|
|
from tests.integrations.django.myapp.wsgi import application
|
|
|
|
+if TYPE_CHECKING:
|
|
+ from types import FrameType
|
|
+ from typing import Optional
|
|
+
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
@@ -283,7 +291,10 @@ def test_query_source_with_in_app_exclude(sentry_init, client, capture_events):
|
|
|
|
@pytest.mark.forked
|
|
@pytest_mark_django_db_decorator(transaction=True)
|
|
-def test_query_source_with_in_app_include(sentry_init, client, capture_events):
|
|
+@pytest.mark.parametrize("django_outside_of_project_root", [False, True])
|
|
+def test_query_source_with_in_app_include(
|
|
+ sentry_init, client, capture_events, django_outside_of_project_root
|
|
+):
|
|
sentry_init(
|
|
integrations=[DjangoIntegration()],
|
|
send_default_pii=True,
|
|
@@ -301,8 +312,43 @@ def test_query_source_with_in_app_include(sentry_init, client, capture_events):
|
|
|
|
events = capture_events()
|
|
|
|
- _, status, _ = unpack_werkzeug_response(client.get(reverse("postgres_select_orm")))
|
|
- assert status == "200 OK"
|
|
+ # Simulate Django installation outside of the project root
|
|
+ original_get_frame_module_abs_path = _get_frame_module_abs_path
|
|
+
|
|
+ def patched_get_frame_module_abs_path_function(frame):
|
|
+ # type: (FrameType) -> Optional[str]
|
|
+ result = original_get_frame_module_abs_path(frame)
|
|
+ if result is None:
|
|
+ return result
|
|
+
|
|
+ namespace = frame.f_globals.get("__name__")
|
|
+ if _module_in_list(namespace, ["django"]):
|
|
+ # Since the result is used as `str` only and not accessed,
|
|
+ # it is sufficient to generate non-existent path
|
|
+ # that would be located outside of the project root.
|
|
+ # Use UNC path for simplicity on Windows.
|
|
+ non_existent_prefix = (
|
|
+ PureWindowsPath("//outside-of-project-root")
|
|
+ if os.name == "nt"
|
|
+ else PurePosixPath("/outside-of-project-root")
|
|
+ )
|
|
+ result = str(non_existent_prefix.joinpath(*Path(result).parts[1:]))
|
|
+ return result
|
|
+
|
|
+ patched_get_frame_module_abs_path = (
|
|
+ mock.patch(
|
|
+ "sentry_sdk.tracing_utils._get_frame_module_abs_path",
|
|
+ patched_get_frame_module_abs_path_function,
|
|
+ )
|
|
+ if django_outside_of_project_root
|
|
+ else contextlib.suppress()
|
|
+ )
|
|
+
|
|
+ with patched_get_frame_module_abs_path:
|
|
+ _, status, _ = unpack_werkzeug_response(
|
|
+ client.get(reverse("postgres_select_orm"))
|
|
+ )
|
|
+ assert status == "200 OK"
|
|
|
|
(event,) = events
|
|
for span in event["spans"]:
|
|
--
|
|
2.45.2
|
|
|