From e85715a0ca19e586f567e79c52f6ed62b5099d3d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 18 Mar 2025 16:07:17 +0100 Subject: [PATCH 01/12] Support Starlette/FastAPI `app.host` (#4157) In Starlette/FastAPI you're able to create subapps. When using `transaction_style="url"` in our integration, this would throw an exception because we try to access `route.path` to determine the transaction name, but `Host` routes have no `path` attribute. Closes https://github.com/getsentry/sentry-python/issues/2631 --- sentry_sdk/integrations/starlette.py | 6 +++- tests/integrations/fastapi/test_fastapi.py | 35 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py index deb05059d5..dbb47dff58 100644 --- a/sentry_sdk/integrations/starlette.py +++ b/sentry_sdk/integrations/starlette.py @@ -693,7 +693,11 @@ def _transaction_name_from_router(scope): for route in router.routes: match = route.matches(scope) if match[0] == Match.FULL: - return route.path + try: + return route.path + except AttributeError: + # routes added via app.host() won't have a path attribute + return scope.get("path") return None diff --git a/tests/integrations/fastapi/test_fastapi.py b/tests/integrations/fastapi/test_fastapi.py index f1c0a69305..4cb9ea1716 100644 --- a/tests/integrations/fastapi/test_fastapi.py +++ b/tests/integrations/fastapi/test_fastapi.py @@ -682,3 +682,38 @@ async def _error(): client.get("/error") assert len(events) == int(expected_error) + + +@pytest.mark.parametrize("transaction_style", ["endpoint", "url"]) +def test_app_host(sentry_init, capture_events, transaction_style): + sentry_init( + traces_sample_rate=1.0, + integrations=[ + StarletteIntegration(transaction_style=transaction_style), + FastApiIntegration(transaction_style=transaction_style), + ], + ) + + app = FastAPI() + subapp = FastAPI() + + @subapp.get("/subapp") + async def subapp_route(): + return {"message": "Hello world!"} + + app.host("subapp", subapp) + + events = capture_events() + + client = TestClient(app) + client.get("/subapp", headers={"Host": "subapp"}) + + assert len(events) == 1 + + (event,) = events + assert "transaction" in event + + if transaction_style == "url": + assert event["transaction"] == "/subapp" + else: + assert event["transaction"].endswith("subapp_route") From bc54a1dbc63240a41ee40e6a20b8a6b2e9e52fa2 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Tue, 18 Mar 2025 16:08:24 +0100 Subject: [PATCH 02/12] feat(tests): Update tox.ini (#4146) Regular `tox.ini` update --- tox.ini | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tox.ini b/tox.ini index 2294fcc00b..40cbf74475 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,7 @@ # The file (and all resulting CI YAMLs) then need to be regenerated via # "scripts/generate-test-files.sh". # -# Last generated: 2025-03-10T11:46:25.287445+00:00 +# Last generated: 2025-03-18T10:29:17.585636+00:00 [tox] requires = @@ -187,12 +187,13 @@ envlist = {py3.6,py3.7}-sqlalchemy-v1.3.9 {py3.6,py3.11,py3.12}-sqlalchemy-v1.4.54 {py3.7,py3.10,py3.11}-sqlalchemy-v2.0.9 - {py3.7,py3.12,py3.13}-sqlalchemy-v2.0.38 + {py3.7,py3.12,py3.13}-sqlalchemy-v2.0.39 # ~~~ Flags ~~~ {py3.8,py3.12,py3.13}-launchdarkly-v9.8.1 {py3.8,py3.12,py3.13}-launchdarkly-v9.9.0 + {py3.8,py3.12,py3.13}-launchdarkly-v9.10.0 {py3.8,py3.12,py3.13}-openfeature-v0.7.5 {py3.9,py3.12,py3.13}-openfeature-v0.8.0 @@ -222,15 +223,14 @@ envlist = {py3.8,py3.10,py3.11}-strawberry-v0.209.8 {py3.8,py3.11,py3.12}-strawberry-v0.227.7 {py3.8,py3.11,py3.12}-strawberry-v0.245.0 - {py3.9,py3.12,py3.13}-strawberry-v0.262.1 + {py3.9,py3.12,py3.13}-strawberry-v0.262.5 # ~~~ Network ~~~ {py3.7,py3.8}-grpc-v1.32.0 {py3.7,py3.9,py3.10}-grpc-v1.44.0 {py3.7,py3.10,py3.11}-grpc-v1.58.3 - {py3.8,py3.12,py3.13}-grpc-v1.70.0 - {py3.9,py3.12,py3.13}-grpc-v1.71.0rc2 + {py3.9,py3.12,py3.13}-grpc-v1.71.0 # ~~~ Tasks ~~~ @@ -294,7 +294,7 @@ envlist = {py3.6,py3.7,py3.8}-trytond-v5.8.16 {py3.8,py3.10,py3.11}-trytond-v6.8.17 {py3.8,py3.11,py3.12}-trytond-v7.0.9 - {py3.8,py3.11,py3.12}-trytond-v7.4.7 + {py3.8,py3.11,py3.12}-trytond-v7.4.8 {py3.7,py3.12,py3.13}-typer-v0.15.2 @@ -578,12 +578,13 @@ deps = sqlalchemy-v1.3.9: sqlalchemy==1.3.9 sqlalchemy-v1.4.54: sqlalchemy==1.4.54 sqlalchemy-v2.0.9: sqlalchemy==2.0.9 - sqlalchemy-v2.0.38: sqlalchemy==2.0.38 + sqlalchemy-v2.0.39: sqlalchemy==2.0.39 # ~~~ Flags ~~~ launchdarkly-v9.8.1: launchdarkly-server-sdk==9.8.1 launchdarkly-v9.9.0: launchdarkly-server-sdk==9.9.0 + launchdarkly-v9.10.0: launchdarkly-server-sdk==9.10.0 openfeature-v0.7.5: openfeature-sdk==0.7.5 openfeature-v0.8.0: openfeature-sdk==0.8.0 @@ -622,7 +623,7 @@ deps = strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8 strawberry-v0.227.7: strawberry-graphql[fastapi,flask]==0.227.7 strawberry-v0.245.0: strawberry-graphql[fastapi,flask]==0.245.0 - strawberry-v0.262.1: strawberry-graphql[fastapi,flask]==0.262.1 + strawberry-v0.262.5: strawberry-graphql[fastapi,flask]==0.262.5 strawberry: httpx @@ -630,8 +631,7 @@ deps = grpc-v1.32.0: grpcio==1.32.0 grpc-v1.44.0: grpcio==1.44.0 grpc-v1.58.3: grpcio==1.58.3 - grpc-v1.70.0: grpcio==1.70.0 - grpc-v1.71.0rc2: grpcio==1.71.0rc2 + grpc-v1.71.0: grpcio==1.71.0 grpc: protobuf grpc: mypy-protobuf grpc: types-protobuf @@ -729,7 +729,7 @@ deps = trytond-v5.8.16: trytond==5.8.16 trytond-v6.8.17: trytond==6.8.17 trytond-v7.0.9: trytond==7.0.9 - trytond-v7.4.7: trytond==7.4.7 + trytond-v7.4.8: trytond==7.4.8 trytond: werkzeug trytond-v4.6.9: werkzeug<1.0 trytond-v4.8.18: werkzeug<1.0 From 11abdd2dba162a44cf4e2d4357752aae69f7ab04 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 19 Mar 2025 08:48:25 +0100 Subject: [PATCH 03/12] Handle loguru msg levels that are not supported by Sentry (#4147) Loguru has two message levels `TRACE` and `SUCCESS` that are not available in Sentry breadcrumbs. This PR maps `TRACE` to `debug` and `SUCCESS` to `info` in Sentry so those breadcrumbs do not show a confusing error message in the Sentry UI. Fixes #2759 --- sentry_sdk/integrations/loguru.py | 36 ++++++++++++++++++++++-- tests/integrations/loguru/test_loguru.py | 23 +++++++-------- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/integrations/loguru.py b/sentry_sdk/integrations/loguru.py index da99dfc4d6..5b76ea812a 100644 --- a/sentry_sdk/integrations/loguru.py +++ b/sentry_sdk/integrations/loguru.py @@ -11,7 +11,7 @@ if TYPE_CHECKING: from logging import LogRecord - from typing import Optional, Tuple + from typing import Optional, Tuple, Any try: import loguru @@ -31,6 +31,16 @@ class LoggingLevels(enum.IntEnum): CRITICAL = 50 +SENTRY_LEVEL_FROM_LOGURU_LEVEL = { + "TRACE": "DEBUG", + "DEBUG": "DEBUG", + "INFO": "INFO", + "SUCCESS": "INFO", + "WARNING": "WARNING", + "ERROR": "ERROR", + "CRITICAL": "CRITICAL", +} + DEFAULT_LEVEL = LoggingLevels.INFO.value DEFAULT_EVENT_LEVEL = LoggingLevels.ERROR.value # We need to save the handlers to be able to remove them later @@ -87,14 +97,34 @@ class _LoguruBaseHandler(_BaseHandler): def _logging_to_event_level(self, record): # type: (LogRecord) -> str try: - return LoggingLevels(record.levelno).name.lower() - except ValueError: + return SENTRY_LEVEL_FROM_LOGURU_LEVEL[ + LoggingLevels(record.levelno).name + ].lower() + except (ValueError, KeyError): return record.levelname.lower() if record.levelname else "" class LoguruEventHandler(_LoguruBaseHandler, EventHandler): """Modified version of :class:`sentry_sdk.integrations.logging.EventHandler` to use loguru's level names.""" + def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None + if kwargs.get("level"): + kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get( + kwargs.get("level", ""), DEFAULT_LEVEL + ) + + super().__init__(*args, **kwargs) + class LoguruBreadcrumbHandler(_LoguruBaseHandler, BreadcrumbHandler): """Modified version of :class:`sentry_sdk.integrations.logging.BreadcrumbHandler` to use loguru's level names.""" + + def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None + if kwargs.get("level"): + kwargs["level"] = SENTRY_LEVEL_FROM_LOGURU_LEVEL.get( + kwargs.get("level", ""), DEFAULT_LEVEL + ) + + super().__init__(*args, **kwargs) diff --git a/tests/integrations/loguru/test_loguru.py b/tests/integrations/loguru/test_loguru.py index 6030108de1..64e9f22ba5 100644 --- a/tests/integrations/loguru/test_loguru.py +++ b/tests/integrations/loguru/test_loguru.py @@ -8,18 +8,18 @@ @pytest.mark.parametrize( - "level,created_event", + "level,created_event,expected_sentry_level", [ # None - no breadcrumb # False - no event # True - event created - (LoggingLevels.TRACE, None), - (LoggingLevels.DEBUG, None), - (LoggingLevels.INFO, False), - (LoggingLevels.SUCCESS, False), - (LoggingLevels.WARNING, False), - (LoggingLevels.ERROR, True), - (LoggingLevels.CRITICAL, True), + (LoggingLevels.TRACE, None, "debug"), + (LoggingLevels.DEBUG, None, "debug"), + (LoggingLevels.INFO, False, "info"), + (LoggingLevels.SUCCESS, False, "info"), + (LoggingLevels.WARNING, False, "warning"), + (LoggingLevels.ERROR, True, "error"), + (LoggingLevels.CRITICAL, True, "critical"), ], ) @pytest.mark.parametrize("disable_breadcrumbs", [True, False]) @@ -29,6 +29,7 @@ def test_just_log( capture_events, level, created_event, + expected_sentry_level, disable_breadcrumbs, disable_events, ): @@ -48,7 +49,7 @@ def test_just_log( formatted_message = ( " | " + "{:9}".format(level.name.upper()) - + "| tests.integrations.loguru.test_loguru:test_just_log:46 - test" + + "| tests.integrations.loguru.test_loguru:test_just_log:47 - test" ) if not created_event: @@ -59,7 +60,7 @@ def test_just_log( not disable_breadcrumbs and created_event is not None ): # not None == not TRACE or DEBUG level (breadcrumb,) = breadcrumbs - assert breadcrumb["level"] == level.name.lower() + assert breadcrumb["level"] == expected_sentry_level assert breadcrumb["category"] == "tests.integrations.loguru.test_loguru" assert breadcrumb["message"][23:] == formatted_message else: @@ -72,7 +73,7 @@ def test_just_log( return (event,) = events - assert event["level"] == (level.name.lower()) + assert event["level"] == expected_sentry_level assert event["logger"] == "tests.integrations.loguru.test_loguru" assert event["logentry"]["message"][23:] == formatted_message From 65132ba2e878edf9734fb90d08ea15d000bb934c Mon Sep 17 00:00:00 2001 From: Simone Locci Date: Wed, 19 Mar 2025 11:05:26 +0100 Subject: [PATCH 04/12] style(integrations): Fix captured typo (#4161) Small typo fix --- sentry_sdk/integrations/logging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 28809de4ab..3777381b83 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -232,10 +232,10 @@ def _emit(self, record): event["logger"] = record.name # Log records from `warnings` module as separate issues - record_caputured_from_warnings_module = ( + record_captured_from_warnings_module = ( record.name == "py.warnings" and record.msg == "%s" ) - if record_caputured_from_warnings_module: + if record_captured_from_warnings_module: # use the actual message and not "%s" as the message # this prevents grouping all warnings under one "%s" issue msg = record.args[0] # type: ignore From 0d3bc3df0f4db5adb1028236d41e951fae17b7e5 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 19 Mar 2025 12:12:59 +0100 Subject: [PATCH 05/12] Reset `DedupeIntegration`'s `last-seen` if `before_send` dropped the event (#4142) Imagine an app throws an exception twice, from different places. The first exception is dropped in the user's `before_send`. The second exception is not. Should the second exception appear in Sentry? The current state is that it won't, since `DedupeIntegration` will take the first, dropped exception into account. When encountering the second exception, it'll consider it a duplicate and will drop it, even though the first exception never made it to Sentry. In this PR, we reset `DedupeIntegration`'s `last-seen` if an event has been dropped by `before_send`, ensuring that the next exception will be reported. Closes https://github.com/getsentry/sentry-python/issues/371 --------- Co-authored-by: Anton Pirker --- sentry_sdk/client.py | 9 +++++++++ sentry_sdk/integrations/dedupe.py | 9 +++++++++ tests/test_basics.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 5bbf919c02..0f97394561 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -37,6 +37,7 @@ ClientConstructor, ) from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, setup_integrations +from sentry_sdk.integrations.dedupe import DedupeIntegration from sentry_sdk.sessions import SessionFlusher from sentry_sdk.envelope import Envelope from sentry_sdk.profiler.continuous_profiler import setup_continuous_profiler @@ -606,6 +607,14 @@ def _prepare_event( self.transport.record_lost_event( "before_send", data_category="error" ) + + # If this is an exception, reset the DedupeIntegration. It still + # remembers the dropped exception as the last exception, meaning + # that if the same exception happens again and is not dropped + # in before_send, it'd get dropped by DedupeIntegration. + if event.get("exception"): + DedupeIntegration.reset_last_seen() + event = new_event before_send_transaction = self.options["before_send_transaction"] diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index be6d9311a3..a115e35292 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -40,3 +40,12 @@ def processor(event, hint): return None integration._last_seen.set(exc) return event + + @staticmethod + def reset_last_seen(): + # type: () -> None + integration = sentry_sdk.get_client().get_integration(DedupeIntegration) + if integration is None: + return + + integration._last_seen.set(None) diff --git a/tests/test_basics.py b/tests/test_basics.py index ad20bb9fd5..d1c3bce2be 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -710,6 +710,37 @@ def test_dedupe_event_processor_drop_records_client_report( assert lost_event_call == ("event_processor", "error", None, 1) +def test_dedupe_doesnt_take_into_account_dropped_exception(sentry_init, capture_events): + # Two exceptions happen one after another. The first one is dropped in the + # user's before_send. The second one isn't. + # Originally, DedupeIntegration would drop the second exception. This test + # is making sure that that is no longer the case -- i.e., DedupeIntegration + # doesn't consider exceptions dropped in before_send. + count = 0 + + def before_send(event, hint): + nonlocal count + count += 1 + if count == 1: + return None + return event + + sentry_init(before_send=before_send) + events = capture_events() + + exc = ValueError("aha!") + for _ in range(2): + # The first ValueError will be dropped by before_send. The second + # ValueError will be accepted by before_send, and should be sent to + # Sentry. + try: + raise exc + except Exception: + capture_exception() + + assert len(events) == 1 + + def test_event_processor_drop_records_client_report( sentry_init, capture_events, capture_record_lost_event_calls ): From f6db98104c1a8aa002bd2ef31a1447e5c79df675 Mon Sep 17 00:00:00 2001 From: viglia Date: Wed, 19 Mar 2025 14:01:40 +0100 Subject: [PATCH 06/12] feat(profiling): reverse profile_session start/stop methods deprecation (#4162) Revert back to using `start_profiler` and `stop_profiler` function names and deprecate the `*_session` ones instead. Prior PR that introduced the change we're undoing: https://github.com/getsentry/sentry-python/pull/4056 --- sentry_sdk/profiler/__init__.py | 8 ++++---- sentry_sdk/profiler/continuous_profiler.py | 20 ++++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/sentry_sdk/profiler/__init__.py b/sentry_sdk/profiler/__init__.py index d8d4e076d5..0bc63e3a6d 100644 --- a/sentry_sdk/profiler/__init__.py +++ b/sentry_sdk/profiler/__init__.py @@ -25,10 +25,10 @@ ) __all__ = [ - "start_profile_session", - "start_profiler", # TODO: Deprecate this in favor of `start_profile_session` - "stop_profile_session", - "stop_profiler", # TODO: Deprecate this in favor of `stop_profile_session` + "start_profile_session", # TODO: Deprecate this in favor of `start_profiler` + "start_profiler", + "stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler` + "stop_profiler", # DEPRECATED: The following was re-exported for backwards compatibility. It # will be removed from sentry_sdk.profiler in a future release. "MAX_PROFILE_DURATION_NS", diff --git a/sentry_sdk/profiler/continuous_profiler.py b/sentry_sdk/profiler/continuous_profiler.py index 9e2aa35fc1..47f63d8f59 100644 --- a/sentry_sdk/profiler/continuous_profiler.py +++ b/sentry_sdk/profiler/continuous_profiler.py @@ -145,32 +145,32 @@ def try_profile_lifecycle_trace_start(): def start_profiler(): # type: () -> None + if _scheduler is None: + return - # TODO: deprecate this as it'll be replaced by `start_profile_session` - start_profile_session() + _scheduler.manual_start() def start_profile_session(): # type: () -> None - if _scheduler is None: - return - _scheduler.manual_start() + # TODO: deprecate this as it'll be replaced by `start_profiler` + start_profiler() def stop_profiler(): # type: () -> None + if _scheduler is None: + return - # TODO: deprecate this as it'll be replaced by `stop_profile_session` - stop_profile_session() + _scheduler.manual_stop() def stop_profile_session(): # type: () -> None - if _scheduler is None: - return - _scheduler.manual_stop() + # TODO: deprecate this as it'll be replaced by `stop_profiler` + stop_profiler() def teardown_continuous_profiler(): From eb189effda67f6ba06f092cb993847ebf0e7347c Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 20 Mar 2025 11:37:25 +0100 Subject: [PATCH 07/12] chore(profiler): Add deprecation warning for session functions (#4171) We're deprecating the short-lived `start_profile_session` and `stop_profile_session` functions in favor of `start_profiler` and `stop_profiler`, respectively. The functions will be dropped in 3.x, see https://github.com/getsentry/sentry-python/pull/4170 --- sentry_sdk/profiler/continuous_profiler.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/profiler/continuous_profiler.py b/sentry_sdk/profiler/continuous_profiler.py index 47f63d8f59..77ba60dbda 100644 --- a/sentry_sdk/profiler/continuous_profiler.py +++ b/sentry_sdk/profiler/continuous_profiler.py @@ -5,6 +5,7 @@ import threading import time import uuid +import warnings from collections import deque from datetime import datetime, timezone @@ -154,7 +155,11 @@ def start_profiler(): def start_profile_session(): # type: () -> None - # TODO: deprecate this as it'll be replaced by `start_profiler` + warnings.warn( + "The `start_profile_session` function is deprecated. Please use `start_profile` instead.", + DeprecationWarning, + stacklevel=2, + ) start_profiler() @@ -169,7 +174,11 @@ def stop_profiler(): def stop_profile_session(): # type: () -> None - # TODO: deprecate this as it'll be replaced by `stop_profiler` + warnings.warn( + "The `stop_profile_session` function is deprecated. Please use `stop_profile` instead.", + DeprecationWarning, + stacklevel=2, + ) stop_profiler() From f76528fa612bc19469813f09612b7dcb448c5b63 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 20 Mar 2025 12:12:20 +0100 Subject: [PATCH 08/12] Fixed flaky test (#4165) The URL www.squirrelchasers.com is actually existing, so we should not access it in our tests. Hope this make the test more stable. --- tests/integrations/stdlib/test_httplib.py | 25 ++++++++--------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/tests/integrations/stdlib/test_httplib.py b/tests/integrations/stdlib/test_httplib.py index 892e07980b..908a22dc6c 100644 --- a/tests/integrations/stdlib/test_httplib.py +++ b/tests/integrations/stdlib/test_httplib.py @@ -398,25 +398,16 @@ def test_http_timeout(monkeypatch, sentry_init, capture_envelopes): envelopes = capture_envelopes() - with start_transaction(op="op", name="name"): - try: - conn = HTTPSConnection("www.squirrelchasers.com") - conn.request("GET", "/top-chasers") + with pytest.raises(TimeoutError): + with start_transaction(op="op", name="name"): + conn = HTTPSConnection("www.example.com") + conn.request("GET", "/bla") conn.getresponse() - except Exception: - pass - - items = [ - item - for envelope in envelopes - for item in envelope.items - if item.type == "transaction" - ] - assert len(items) == 1 - - transaction = items[0].payload.json + + (transaction_envelope,) = envelopes + transaction = transaction_envelope.get_transaction_event() assert len(transaction["spans"]) == 1 span = transaction["spans"][0] assert span["op"] == "http.client" - assert span["description"] == "GET https://www.squirrelchasers.com/top-chasers" + assert span["description"] == "GET https://www.example.com/bla" From 2579cb28e24b5a75a7b8b76fb8849539726ae032 Mon Sep 17 00:00:00 2001 From: Emmanuel Ferdman Date: Thu, 20 Mar 2025 15:05:03 +0200 Subject: [PATCH 09/12] Update scripts sources (#4166) # PR Summary Small PR - Commit d4f4130ad9e2c5c24c06c50855aa0b55fa407a11 moved scripts. This PR adjusts sources to changes. Signed-off-by: Emmanuel Ferdman --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 085dbd6075..024a374f85 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -182,14 +182,14 @@ You need to have an AWS account and AWS CLI installed and setup. We put together two helper functions that can help you with development: -- `./scripts/aws-deploy-local-layer.sh` +- `./scripts/aws/aws-deploy-local-layer.sh` - This script [scripts/aws-deploy-local-layer.sh](scripts/aws-deploy-local-layer.sh) will take the code you have checked out locally, create a Lambda layer out of it and deploy it to the `eu-central-1` region of your configured AWS account using `aws` CLI. + This script [scripts/aws/aws-deploy-local-layer.sh](scripts/aws/aws-deploy-local-layer.sh) will take the code you have checked out locally, create a Lambda layer out of it and deploy it to the `eu-central-1` region of your configured AWS account using `aws` CLI. The Lambda layer will have the name `SentryPythonServerlessSDK-local-dev` -- `./scripts/aws-attach-layer-to-lambda-function.sh` +- `./scripts/aws/aws-attach-layer-to-lambda-function.sh` - You can use this script [scripts/aws-attach-layer-to-lambda-function.sh](scripts/aws-attach-layer-to-lambda-function.sh) to attach the Lambda layer you just deployed (using the first script) onto one of your existing Lambda functions. You will have to give the name of the Lambda function to attach onto as an argument. (See the script for details.) + You can use this script [scripts/aws/aws-attach-layer-to-lambda-function.sh](scripts/aws/aws-attach-layer-to-lambda-function.sh) to attach the Lambda layer you just deployed (using the first script) onto one of your existing Lambda functions. You will have to give the name of the Lambda function to attach onto as an argument. (See the script for details.) With these two helper scripts it should be easy to rapidly iterate your development on the Lambda layer. From 5715734eac1c5fb4b6ec61ef459080c74fa777b5 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Thu, 20 Mar 2025 14:06:10 +0100 Subject: [PATCH 10/12] Fix memory leak by not piling up breadcrumbs forever in Spark workers. (#4167) We now clear all existing breadcrumbs when a job is started. If an error happens in a job, only breadcrumbs created in this job will be shown. Fixes #1245. --- sentry_sdk/integrations/spark/spark_driver.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/sentry_sdk/integrations/spark/spark_driver.py b/sentry_sdk/integrations/spark/spark_driver.py index 701ba12d89..fac985357f 100644 --- a/sentry_sdk/integrations/spark/spark_driver.py +++ b/sentry_sdk/integrations/spark/spark_driver.py @@ -31,9 +31,13 @@ def _set_app_properties(): spark_context = SparkContext._active_spark_context if spark_context: - spark_context.setLocalProperty("sentry_app_name", spark_context.appName) spark_context.setLocalProperty( - "sentry_application_id", spark_context.applicationId + "sentry_app_name", + spark_context.appName, + ) + spark_context.setLocalProperty( + "sentry_application_id", + spark_context.applicationId, ) @@ -231,12 +235,14 @@ def _add_breadcrumb( data=None, # type: Optional[dict[str, Any]] ): # type: (...) -> None - sentry_sdk.get_global_scope().add_breadcrumb( + sentry_sdk.get_isolation_scope().add_breadcrumb( level=level, message=message, data=data ) def onJobStart(self, jobStart): # noqa: N802,N803 # type: (Any) -> None + sentry_sdk.get_isolation_scope().clear_breadcrumbs() + message = "Job {} Started".format(jobStart.jobId()) self._add_breadcrumb(level="info", message=message) _set_app_properties() From 12b3ca39ca48dc611207a77c63659b3a93d88445 Mon Sep 17 00:00:00 2001 From: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> Date: Thu, 20 Mar 2025 17:31:21 +0100 Subject: [PATCH 11/12] fix(tracing): Fix `InvalidOperation` (#4179) `InvalidOperation` can occur when using tracing if the `Decimal` class's global context has been modified to set the precision below 6. This change fixes this bug by setting a custom context for our `quantize` call. Fixes #4177 --- sentry_sdk/tracing_utils.py | 8 ++++++-- tests/tracing/test_sample_rand.py | 26 ++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index 6aa4e4882a..ba56695740 100644 --- a/sentry_sdk/tracing_utils.py +++ b/sentry_sdk/tracing_utils.py @@ -5,7 +5,7 @@ import sys from collections.abc import Mapping from datetime import timedelta -from decimal import ROUND_DOWN, Decimal +from decimal import ROUND_DOWN, Context, Decimal from functools import wraps from random import Random from urllib.parse import quote, unquote @@ -871,7 +871,11 @@ def _generate_sample_rand( sample_rand = rng.uniform(lower, upper) # Round down to exactly six decimal-digit precision. - return Decimal(sample_rand).quantize(Decimal("0.000001"), rounding=ROUND_DOWN) + # Setting the context is needed to avoid an InvalidOperation exception + # in case the user has changed the default precision. + return Decimal(sample_rand).quantize( + Decimal("0.000001"), rounding=ROUND_DOWN, context=Context(prec=6) + ) def _sample_rand_range(parent_sampled, sample_rate): diff --git a/tests/tracing/test_sample_rand.py b/tests/tracing/test_sample_rand.py index b8f5c042ed..ef277a3dec 100644 --- a/tests/tracing/test_sample_rand.py +++ b/tests/tracing/test_sample_rand.py @@ -1,3 +1,4 @@ +import decimal from unittest import mock import pytest @@ -53,3 +54,28 @@ def test_transaction_uses_incoming_sample_rand( # Transaction event captured if sample_rand < sample_rate, indicating that # sample_rand is used to make the sampling decision. assert len(events) == int(sample_rand < sample_rate) + + +def test_decimal_context(sentry_init, capture_events): + """ + Ensure that having a decimal context with a precision below 6 + does not cause an InvalidOperation exception. + """ + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + old_prec = decimal.getcontext().prec + decimal.getcontext().prec = 2 + + try: + with mock.patch( + "sentry_sdk.tracing_utils.Random.uniform", return_value=0.123456789 + ): + with sentry_sdk.start_transaction() as transaction: + assert ( + transaction.get_baggage().sentry_items["sample_rand"] == "0.123456" + ) + finally: + decimal.getcontext().prec = old_prec + + assert len(events) == 1 From a3356d7808d3f07ce68a9362efb8d226d080310a Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Fri, 21 Mar 2025 08:59:21 +0000 Subject: [PATCH 12/12] release: 2.24.0 --- CHANGELOG.md | 16 ++++++++++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf4da0e29..95ae3f3e96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 2.24.0 + +### Various fixes & improvements + +- fix(tracing): Fix `InvalidOperation` (#4179) by @szokeasaurusrex +- Fix memory leak by not piling up breadcrumbs forever in Spark workers. (#4167) by @antonpirker +- Update scripts sources (#4166) by @emmanuel-ferdman +- Fixed flaky test (#4165) by @antonpirker +- chore(profiler): Add deprecation warning for session functions (#4171) by @sentrivana +- feat(profiling): reverse profile_session start/stop methods deprecation (#4162) by @viglia +- Reset `DedupeIntegration`'s `last-seen` if `before_send` dropped the event (#4142) by @sentrivana +- style(integrations): Fix captured typo (#4161) by @pimuzzo +- Handle loguru msg levels that are not supported by Sentry (#4147) by @antonpirker +- feat(tests): Update tox.ini (#4146) by @sentrivana +- Support Starlette/FastAPI `app.host` (#4157) by @sentrivana + ## 2.23.1 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index 9408338941..38772762e1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -31,7 +31,7 @@ copyright = "2019-{}, Sentry Team and Contributors".format(datetime.now().year) author = "Sentry Team and Contributors" -release = "2.23.1" +release = "2.24.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index a24903e0ff..d20badf9ed 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -965,4 +965,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.23.1" +VERSION = "2.24.0" diff --git a/setup.py b/setup.py index a134913fe4..9c33703ac8 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.23.1", + version="2.24.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python",