From 575cc93316f0574852efde56e5d61278f3a41232 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Mon, 19 Feb 2024 13:14:18 +0000 Subject: [PATCH 1/5] release: 1.40.5 --- CHANGELOG.md | 9 +++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3df6e30d87..25c7b1579b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.40.5 + +### Various fixes & improvements + +- Deprecate `last_event_id()`. (#2749) by @antonpirker +- ref(uwsgi): Warn if uWSGI is set up without proper thread support (#2738) by @sentrivana +- fix(aiohttp): `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/docs/conf.py b/docs/conf.py index 45b465c615..8787c30934 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -30,7 +30,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 ad7b1099ae..e20625cfa1 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -316,4 +316,4 @@ def _get_default_options(): del _get_default_options -VERSION = "1.40.4" +VERSION = "1.40.5" diff --git a/setup.py b/setup.py index a118cfb20c..d1bdb16201 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", From 3a3e3803a2b83c35bef0380ebd4cebc84afec51a Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Mon, 19 Feb 2024 14:14:50 +0100 Subject: [PATCH 2/5] Update CHANGELOG.md --- CHANGELOG.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c7b1579b..6eef10e114 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,15 @@ ### Various fixes & improvements - Deprecate `last_event_id()`. (#2749) by @antonpirker -- ref(uwsgi): Warn if uWSGI is set up without proper thread support (#2738) by @sentrivana -- fix(aiohttp): `parsed_url` can be `None` (#2734) by @sentrivana +- 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 From e24508f94f1322bc95286d992e0ce3b9e5be3e7f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 14:20:47 +0000 Subject: [PATCH 3/5] build(deps): bump checkouts/data-schemas from `6121fd3` to `eb941c2` (#2747) Bumps [checkouts/data-schemas](https://github.com/getsentry/sentry-data-schemas) from `6121fd3` to `eb941c2`. - [Commits](https://github.com/getsentry/sentry-data-schemas/compare/6121fd368469c498515c13feb9c28a804ef42e2e...eb941c2dcbcff9bc04f35ce7f1837de118f790fe) --- updated-dependencies: - dependency-name: checkouts/data-schemas dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Ivana Kellyerova --- checkouts/data-schemas | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e07c0ac6d4bfb47ae33b316c591be2f4cd0fc393 Mon Sep 17 00:00:00 2001 From: Ivana Kellyerova Date: Wed, 21 Feb 2024 11:27:12 +0100 Subject: [PATCH 4/5] Support clickhouse-driver==0.2.7 (#2752) --- sentry_sdk/integrations/clickhouse_driver.py | 5 +++++ 1 file changed, 5 insertions(+) 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 ) From 2eeb8c50a0fe987cf70ef254ea0d63bf422a1899 Mon Sep 17 00:00:00 2001 From: George Gritsouk <989898+gggritso@users.noreply.github.com> Date: Wed, 21 Feb 2024 05:47:17 -0500 Subject: [PATCH 5/5] 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. --- sentry_sdk/tracing_utils.py | 5 +- tests/integrations/asyncpg/__init__.py | 6 ++ .../asyncpg/asyncpg_helpers/__init__.py | 0 .../asyncpg/asyncpg_helpers/helpers.py | 2 + tests/integrations/asyncpg/test_asyncpg.py | 51 ++++++++++++++ tests/integrations/django/__init__.py | 6 ++ .../django/django_helpers/__init__.py | 0 .../django/django_helpers/views.py | 9 +++ tests/integrations/django/myapp/urls.py | 6 ++ .../integrations/django/test_db_query_data.py | 57 ++++++++++++++++ tests/integrations/sqlalchemy/__init__.py | 6 ++ .../sqlalchemy/sqlalchemy_helpers/__init__.py | 0 .../sqlalchemy/sqlalchemy_helpers/helpers.py | 7 ++ .../sqlalchemy/test_sqlalchemy.py | 68 +++++++++++++++++++ tox.ini | 1 + 15 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 tests/integrations/asyncpg/asyncpg_helpers/__init__.py create mode 100644 tests/integrations/asyncpg/asyncpg_helpers/helpers.py create mode 100644 tests/integrations/django/django_helpers/__init__.py create mode 100644 tests/integrations/django/django_helpers/views.py create mode 100644 tests/integrations/sqlalchemy/sqlalchemy_helpers/__init__.py create mode 100644 tests/integrations/sqlalchemy/sqlalchemy_helpers/helpers.py diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index bc0ddc51d5..98cdec5e38 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -7,6 +7,7 @@ from sentry_sdk.consts import OP, SPANDATA from sentry_sdk.utils import ( capture_internal_exceptions, + filename_for_module, Dsn, match_regex_list, to_string, @@ -255,7 +256,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/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 705ac83dbc..a839031c3b 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 import asyncpg @@ -592,6 +593,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 706be13c3a..92621b07a2 100644 --- a/tests/integrations/django/myapp/urls.py +++ b/tests/integrations/django/myapp/urls.py @@ -26,6 +26,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"), @@ -59,6 +60,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 cf2ef57358..92b1415f78 100644 --- a/tests/integrations/django/test_db_query_data.py +++ b/tests/integrations/django/test_db_query_data.py @@ -4,6 +4,7 @@ import pytest from datetime import datetime +from sentry_sdk._compat import PY2 from django import VERSION as DJANGO_VERSION from django.db import connections @@ -168,6 +169,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 3f196cd0b9..08c8e29ec4 100644 --- a/tests/integrations/sqlalchemy/test_sqlalchemy.py +++ b/tests/integrations/sqlalchemy/test_sqlalchemy.py @@ -3,6 +3,7 @@ import sys from datetime import datetime +from sentry_sdk._compat import PY2 from sqlalchemy import Column, ForeignKey, Integer, String, create_engine from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.declarative import declarative_base @@ -449,6 +450,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 90806b4220..34870b1ada 100644 --- a/tox.ini +++ b/tox.ini @@ -577,6 +577,7 @@ deps = setenv = PYTHONDONTWRITEBYTECODE=1 + OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES common: TESTPATH=tests gevent: TESTPATH=tests aiohttp: TESTPATH=tests/integrations/aiohttp