python-sentry-sdk/0006-test-tracing-Test-add_query_source-with-modules-outs.patch
Roman Inflianskas 304f1d0bd2
Run integration tests with a PostgreSQL server
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]
2024-08-20 08:59:19 +03:00

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