diff --git a/CHANGELOG.md b/CHANGELOG.md index 3df6e30d87..6eef10e114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 1.40.5 + +### Various fixes & improvements + +- Deprecate `last_event_id()`. (#2749) by @antonpirker +- Warn if uWSGI is set up without proper thread support (#2738) by @sentrivana + + uWSGI has to be run in threaded mode for the SDK to run properly. If this is + not the case, the consequences could range from features not working unexpectedly + to uWSGI workers crashing. + + Please make sure to run uWSGI with both `--enable-threads` and `--py-call-uwsgi-fork-hooks`. + +- `parsed_url` can be `None` (#2734) by @sentrivana +- Python 3.7 is not supported anymore by Lambda, so removed it and added 3.12 (#2729) by @antonpirker + ## 1.40.4 ### Various fixes & improvements diff --git a/checkouts/data-schemas b/checkouts/data-schemas index 6121fd3684..eb941c2dcb 160000 --- a/checkouts/data-schemas +++ b/checkouts/data-schemas @@ -1 +1 @@ -Subproject commit 6121fd368469c498515c13feb9c28a804ef42e2e +Subproject commit eb941c2dcbcff9bc04f35ce7f1837de118f790fe diff --git a/docs/conf.py b/docs/conf.py index 9d312db4a8..3012cb2c38 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,7 +28,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "1.40.4" +release = "1.40.5" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index ac6d48e9cc..e4fc989f68 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -326,4 +326,4 @@ def _get_default_options(): del _get_default_options -VERSION = "1.40.4" +VERSION = "1.40.5" diff --git a/sentry_sdk/integrations/clickhouse_driver.py b/sentry_sdk/integrations/clickhouse_driver.py index f0955ff756..a09e567118 100644 --- a/sentry_sdk/integrations/clickhouse_driver.py +++ b/sentry_sdk/integrations/clickhouse_driver.py @@ -59,6 +59,11 @@ def setup_once() -> None: clickhouse_driver.client.Client.receive_end_of_query = _wrap_end( clickhouse_driver.client.Client.receive_end_of_query ) + if hasattr(clickhouse_driver.client.Client, "receive_end_of_insert_query"): + # In 0.2.7, insert queries are handled separately via `receive_end_of_insert_query` + clickhouse_driver.client.Client.receive_end_of_insert_query = _wrap_end( + clickhouse_driver.client.Client.receive_end_of_insert_query + ) clickhouse_driver.client.Client.receive_result = _wrap_end( clickhouse_driver.client.Client.receive_result ) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 537a4d279f..3734283973 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -12,6 +12,7 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.utils import ( capture_internal_exceptions, + filename_for_module, Dsn, logger, match_regex_list, @@ -250,7 +251,9 @@ def add_query_source(hub, span): except Exception: filepath = None if filepath is not None: - if project_root is not None and filepath.startswith(project_root): + if namespace is not None and not PY2: + in_app_path = filename_for_module(namespace, filepath) + elif project_root is not None and filepath.startswith(project_root): in_app_path = filepath.replace(project_root, "").lstrip(os.sep) else: in_app_path = filepath diff --git a/setup.py b/setup.py index e59f20629b..0392825775 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="1.40.4", + version="1.40.5", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", diff --git a/tests/integrations/asyncpg/__init__.py b/tests/integrations/asyncpg/__init__.py index 50f607f3a6..d988407a2d 100644 --- a/tests/integrations/asyncpg/__init__.py +++ b/tests/integrations/asyncpg/__init__.py @@ -1,4 +1,10 @@ +import os +import sys import pytest pytest.importorskip("asyncpg") pytest.importorskip("pytest_asyncio") + +# Load `asyncpg_helpers` into the module search path to test query source path names relative to module. See +# `test_query_source_with_module_in_search_path` +sys.path.insert(0, os.path.join(os.path.dirname(__file__))) diff --git a/tests/integrations/asyncpg/asyncpg_helpers/__init__.py b/tests/integrations/asyncpg/asyncpg_helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/asyncpg/asyncpg_helpers/helpers.py b/tests/integrations/asyncpg/asyncpg_helpers/helpers.py new file mode 100644 index 0000000000..8de809ba1b --- /dev/null +++ b/tests/integrations/asyncpg/asyncpg_helpers/helpers.py @@ -0,0 +1,2 @@ +async def execute_query_in_connection(query, connection): + await connection.execute(query) diff --git a/tests/integrations/asyncpg/test_asyncpg.py b/tests/integrations/asyncpg/test_asyncpg.py index c7fb05fdf0..6c2d142ef0 100644 --- a/tests/integrations/asyncpg/test_asyncpg.py +++ b/tests/integrations/asyncpg/test_asyncpg.py @@ -19,6 +19,7 @@ PG_PORT = 5432 +from sentry_sdk._compat import PY2 import datetime from contextlib import contextmanager from unittest import mock @@ -586,6 +587,56 @@ async def test_query_source(sentry_init, capture_events): assert data.get(SPANDATA.CODE_FUNCTION) == "test_query_source" +@pytest.mark.asyncio +async def test_query_source_with_module_in_search_path(sentry_init, capture_events): + """ + Test that query source is relative to the path of the module it ran in + """ + sentry_init( + integrations=[AsyncPGIntegration()], + enable_tracing=True, + enable_db_query_source=True, + db_query_source_threshold_ms=0, + ) + + events = capture_events() + + from asyncpg_helpers.helpers import execute_query_in_connection + + with start_transaction(name="test_transaction", sampled=True): + conn: Connection = await connect(PG_CONNECTION_URI) + + await execute_query_in_connection( + "INSERT INTO users(name, password, dob) VALUES ('Alice', 'secret', '1990-12-25')", + conn, + ) + + await conn.close() + + (event,) = events + + span = event["spans"][-1] + assert span["description"].startswith("INSERT INTO") + + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + if not PY2: + assert data.get(SPANDATA.CODE_NAMESPACE) == "asyncpg_helpers.helpers" + assert data.get(SPANDATA.CODE_FILEPATH) == "asyncpg_helpers/helpers.py" + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "execute_query_in_connection" + + @pytest.mark.asyncio async def test_no_query_source_if_duration_too_short(sentry_init, capture_events): sentry_init( diff --git a/tests/integrations/django/__init__.py b/tests/integrations/django/__init__.py index 70cc4776d5..41d72f92a5 100644 --- a/tests/integrations/django/__init__.py +++ b/tests/integrations/django/__init__.py @@ -1,3 +1,9 @@ +import os +import sys import pytest pytest.importorskip("django") + +# Load `django_helpers` into the module search path to test query source path names relative to module. See +# `test_query_source_with_module_in_search_path` +sys.path.insert(0, os.path.join(os.path.dirname(__file__))) diff --git a/tests/integrations/django/django_helpers/__init__.py b/tests/integrations/django/django_helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/django/django_helpers/views.py b/tests/integrations/django/django_helpers/views.py new file mode 100644 index 0000000000..a5759a5199 --- /dev/null +++ b/tests/integrations/django/django_helpers/views.py @@ -0,0 +1,9 @@ +from django.contrib.auth.models import User +from django.http import HttpResponse +from django.views.decorators.csrf import csrf_exempt + + +@csrf_exempt +def postgres_select_orm(request, *args, **kwargs): + user = User.objects.using("postgres").all().first() + return HttpResponse("ok {}".format(user)) diff --git a/tests/integrations/django/myapp/urls.py b/tests/integrations/django/myapp/urls.py index fc4bf5615d..6f23e9aeec 100644 --- a/tests/integrations/django/myapp/urls.py +++ b/tests/integrations/django/myapp/urls.py @@ -24,6 +24,7 @@ def path(path, *args, **kwargs): from . import views +from django_helpers import views as helper_views urlpatterns = [ path("view-exc", views.view_exc, name="view_exc"), @@ -57,6 +58,11 @@ def path(path, *args, **kwargs): path("template-test3", views.template_test3, name="template_test3"), path("postgres-select", views.postgres_select, name="postgres_select"), path("postgres-select-slow", views.postgres_select_orm, name="postgres_select_orm"), + path( + "postgres-select-slow-from-supplement", + helper_views.postgres_select_orm, + name="postgres_select_slow_from_supplement", + ), path( "permission-denied-exc", views.permission_denied_exc, diff --git a/tests/integrations/django/test_db_query_data.py b/tests/integrations/django/test_db_query_data.py index 96e02cfa0f..7121f0c60d 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -4,6 +4,7 @@ from datetime import datetime from unittest import mock +from sentry_sdk._compat import PY2 from django import VERSION as DJANGO_VERSION from django.db import connections @@ -163,6 +164,62 @@ def test_query_source(sentry_init, client, capture_events): raise AssertionError("No db span found") +@pytest.mark.forked +@pytest_mark_django_db_decorator(transaction=True) +def test_query_source_with_module_in_search_path(sentry_init, client, capture_events): + """ + Test that query source is relative to the path of the module it ran in + """ + client = Client(application) + + sentry_init( + integrations=[DjangoIntegration()], + send_default_pii=True, + traces_sample_rate=1.0, + enable_db_query_source=True, + db_query_source_threshold_ms=0, + ) + + if "postgres" not in connections: + pytest.skip("postgres tests disabled") + + # trigger Django to open a new connection by marking the existing one as None. + connections["postgres"].connection = None + + events = capture_events() + + _, status, _ = unpack_werkzeug_response( + client.get(reverse("postgres_select_slow_from_supplement")) + ) + assert status == "200 OK" + + (event,) = events + for span in event["spans"]: + if span.get("op") == "db" and "auth_user" in span.get("description"): + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + + if not PY2: + assert data.get(SPANDATA.CODE_NAMESPACE) == "django_helpers.views" + assert data.get(SPANDATA.CODE_FILEPATH) == "django_helpers/views.py" + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "postgres_select_orm" + + break + else: + raise AssertionError("No db span found") + + @pytest.mark.forked @pytest_mark_django_db_decorator(transaction=True) def test_query_source_with_in_app_exclude(sentry_init, client, capture_events): diff --git a/tests/integrations/sqlalchemy/__init__.py b/tests/integrations/sqlalchemy/__init__.py index b430bf6d43..33c43a6872 100644 --- a/tests/integrations/sqlalchemy/__init__.py +++ b/tests/integrations/sqlalchemy/__init__.py @@ -1,3 +1,9 @@ +import os +import sys import pytest pytest.importorskip("sqlalchemy") + +# Load `sqlalchemy_helpers` into the module search path to test query source path names relative to module. See +# `test_query_source_with_module_in_search_path` +sys.path.insert(0, os.path.join(os.path.dirname(__file__))) diff --git a/tests/integrations/sqlalchemy/sqlalchemy_helpers/__init__.py b/tests/integrations/sqlalchemy/sqlalchemy_helpers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integrations/sqlalchemy/sqlalchemy_helpers/helpers.py b/tests/integrations/sqlalchemy/sqlalchemy_helpers/helpers.py new file mode 100644 index 0000000000..ca65a88d25 --- /dev/null +++ b/tests/integrations/sqlalchemy/sqlalchemy_helpers/helpers.py @@ -0,0 +1,7 @@ +def add_model_to_session(model, session): + session.add(model) + session.commit() + + +def query_first_model_from_session(model_klass, session): + return session.query(model_klass).first() diff --git a/tests/integrations/sqlalchemy/test_sqlalchemy.py b/tests/integrations/sqlalchemy/test_sqlalchemy.py index bc548733d8..6196583583 100644 --- a/tests/integrations/sqlalchemy/test_sqlalchemy.py +++ b/tests/integrations/sqlalchemy/test_sqlalchemy.py @@ -438,6 +438,73 @@ class Person(Base): raise AssertionError("No db span found") +def test_query_source_with_module_in_search_path(sentry_init, capture_events): + """ + Test that query source is relative to the path of the module it ran in + """ + sentry_init( + integrations=[SqlalchemyIntegration()], + enable_tracing=True, + enable_db_query_source=True, + db_query_source_threshold_ms=0, + ) + events = capture_events() + + from sqlalchemy_helpers.helpers import ( + add_model_to_session, + query_first_model_from_session, + ) + + with start_transaction(name="test_transaction", sampled=True): + Base = declarative_base() # noqa: N806 + + class Person(Base): + __tablename__ = "person" + id = Column(Integer, primary_key=True) + name = Column(String(250), nullable=False) + + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + + Session = sessionmaker(bind=engine) # noqa: N806 + session = Session() + + bob = Person(name="Bob") + + add_model_to_session(bob, session) + + assert query_first_model_from_session(Person, session) == bob + + (event,) = events + + for span in event["spans"]: + if span.get("op") == "db" and span.get("description").startswith( + "SELECT person" + ): + data = span.get("data", {}) + + assert SPANDATA.CODE_LINENO in data + assert SPANDATA.CODE_NAMESPACE in data + assert SPANDATA.CODE_FILEPATH in data + assert SPANDATA.CODE_FUNCTION in data + + assert type(data.get(SPANDATA.CODE_LINENO)) == int + assert data.get(SPANDATA.CODE_LINENO) > 0 + if not PY2: + assert data.get(SPANDATA.CODE_NAMESPACE) == "sqlalchemy_helpers.helpers" + assert ( + data.get(SPANDATA.CODE_FILEPATH) == "sqlalchemy_helpers/helpers.py" + ) + + is_relative_path = data.get(SPANDATA.CODE_FILEPATH)[0] != os.sep + assert is_relative_path + + assert data.get(SPANDATA.CODE_FUNCTION) == "query_first_model_from_session" + break + else: + raise AssertionError("No db span found") + + def test_no_query_source_if_duration_too_short(sentry_init, capture_events): sentry_init( integrations=[SqlalchemyIntegration()], diff --git a/tox.ini b/tox.ini index 78bebf4a00..8a2ba6af1a 100644 --- a/tox.ini +++ b/tox.ini @@ -562,6 +562,7 @@ deps = setenv = PYTHONDONTWRITEBYTECODE=1 + OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES common: TESTPATH=tests gevent: TESTPATH=tests aiohttp: TESTPATH=tests/integrations/aiohttp