From f3687fcbd367187c395a802a98ce7eb275239ca1 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Thu, 17 Apr 2025 08:24:49 -0500 Subject: [PATCH 01/10] feat(spans): Record flag evaluations as span attributes (#4280) Flags evaluated within a span are appended to the span as attributes. --------- Co-authored-by: Daniel Szoke <7881302+szokeasaurusrex@users.noreply.github.com> --- sentry_sdk/feature_flags.py | 4 ++ sentry_sdk/integrations/launchdarkly.py | 6 +-- sentry_sdk/integrations/openfeature.py | 8 ++-- sentry_sdk/integrations/unleash.py | 5 +-- sentry_sdk/tracing.py | 13 +++++- .../launchdarkly/test_launchdarkly.py | 41 +++++++++++++++++++ .../openfeature/test_openfeature.py | 26 ++++++++++++ tests/integrations/statsig/test_statsig.py | 20 +++++++++ tests/integrations/unleash/test_unleash.py | 20 +++++++++ tests/test_feature_flags.py | 39 ++++++++++++++++++ 10 files changed, 170 insertions(+), 12 deletions(-) diff --git a/sentry_sdk/feature_flags.py b/sentry_sdk/feature_flags.py index a0b1338356..dd8d41c32e 100644 --- a/sentry_sdk/feature_flags.py +++ b/sentry_sdk/feature_flags.py @@ -66,3 +66,7 @@ def add_feature_flag(flag, result): """ flags = sentry_sdk.get_current_scope().flags flags.set(flag, result) + + span = sentry_sdk.get_current_span() + if span: + span.set_flag(f"flag.evaluation.{flag}", result) diff --git a/sentry_sdk/integrations/launchdarkly.py b/sentry_sdk/integrations/launchdarkly.py index cb9e911463..d3c423e7be 100644 --- a/sentry_sdk/integrations/launchdarkly.py +++ b/sentry_sdk/integrations/launchdarkly.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -import sentry_sdk +from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations import DidNotEnable, Integration try: @@ -53,8 +53,8 @@ def metadata(self): def after_evaluation(self, series_context, data, detail): # type: (EvaluationSeriesContext, dict[Any, Any], EvaluationDetail) -> dict[Any, Any] if isinstance(detail.value, bool): - flags = sentry_sdk.get_current_scope().flags - flags.set(series_context.key, detail.value) + add_feature_flag(series_context.key, detail.value) + return data def before_evaluation(self, series_context, data): diff --git a/sentry_sdk/integrations/openfeature.py b/sentry_sdk/integrations/openfeature.py index bf66b94e8b..e2b33d83f2 100644 --- a/sentry_sdk/integrations/openfeature.py +++ b/sentry_sdk/integrations/openfeature.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING -import sentry_sdk +from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations import DidNotEnable, Integration try: @@ -29,11 +29,9 @@ class OpenFeatureHook(Hook): def after(self, hook_context, details, hints): # type: (HookContext, FlagEvaluationDetails[bool], HookHints) -> None if isinstance(details.value, bool): - flags = sentry_sdk.get_current_scope().flags - flags.set(details.flag_key, details.value) + add_feature_flag(details.flag_key, details.value) def error(self, hook_context, exception, hints): # type: (HookContext, Exception, HookHints) -> None if isinstance(hook_context.default_value, bool): - flags = sentry_sdk.get_current_scope().flags - flags.set(hook_context.flag_key, hook_context.default_value) + add_feature_flag(hook_context.flag_key, hook_context.default_value) diff --git a/sentry_sdk/integrations/unleash.py b/sentry_sdk/integrations/unleash.py index 873f36c68b..6daa0a411f 100644 --- a/sentry_sdk/integrations/unleash.py +++ b/sentry_sdk/integrations/unleash.py @@ -1,7 +1,7 @@ from functools import wraps from typing import Any -import sentry_sdk +from sentry_sdk.feature_flags import add_feature_flag from sentry_sdk.integrations import Integration, DidNotEnable try: @@ -26,8 +26,7 @@ def sentry_is_enabled(self, feature, *args, **kwargs): # We have no way of knowing what type of unleash feature this is, so we have to treat # it as a boolean / toggle feature. - flags = sentry_sdk.get_current_scope().flags - flags.set(feature, enabled) + add_feature_flag(feature, enabled) return enabled diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 13d9f63d5e..ae0b90253e 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -278,6 +278,8 @@ class Span: "scope", "origin", "name", + "_flags", + "_flags_capacity", ) def __init__( @@ -313,6 +315,8 @@ def __init__( self._tags = {} # type: MutableMapping[str, str] self._data = {} # type: Dict[str, Any] self._containing_transaction = containing_transaction + self._flags = {} # type: Dict[str, bool] + self._flags_capacity = 10 if hub is not None: warnings.warn( @@ -597,6 +601,11 @@ def set_data(self, key, value): # type: (str, Any) -> None self._data[key] = value + def set_flag(self, flag, result): + # type: (str, bool) -> None + if len(self._flags) < self._flags_capacity: + self._flags[flag] = result + def set_status(self, value): # type: (str) -> None self.status = value @@ -700,7 +709,9 @@ def to_json(self): if tags: rv["tags"] = tags - data = self._data + data = {} + data.update(self._flags) + data.update(self._data) if data: rv["data"] = data diff --git a/tests/integrations/launchdarkly/test_launchdarkly.py b/tests/integrations/launchdarkly/test_launchdarkly.py index 20566ce09a..20bb4d031f 100644 --- a/tests/integrations/launchdarkly/test_launchdarkly.py +++ b/tests/integrations/launchdarkly/test_launchdarkly.py @@ -12,6 +12,8 @@ import sentry_sdk from sentry_sdk.integrations import DidNotEnable from sentry_sdk.integrations.launchdarkly import LaunchDarklyIntegration +from sentry_sdk import start_span, start_transaction +from tests.conftest import ApproxDict @pytest.mark.parametrize( @@ -202,3 +204,42 @@ def test_launchdarkly_integration_did_not_enable(monkeypatch): monkeypatch.setattr(client, "is_initialized", lambda: False) with pytest.raises(DidNotEnable): LaunchDarklyIntegration(ld_client=client) + + +@pytest.mark.parametrize( + "use_global_client", + (False, True), +) +def test_launchdarkly_span_integration( + sentry_init, use_global_client, capture_events, uninstall_integration +): + td = TestData.data_source() + td.update(td.flag("hello").variation_for_all(True)) + # Disable background requests as we aren't using a server. + config = Config( + "sdk-key", update_processor_class=td, diagnostic_opt_out=True, send_events=False + ) + + uninstall_integration(LaunchDarklyIntegration.identifier) + if use_global_client: + ldclient.set_config(config) + sentry_init(traces_sample_rate=1.0, integrations=[LaunchDarklyIntegration()]) + client = ldclient.get() + else: + client = LDClient(config=config) + sentry_init( + traces_sample_rate=1.0, + integrations=[LaunchDarklyIntegration(ld_client=client)], + ) + + events = capture_events() + + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + client.variation("hello", Context.create("my-org", "organization"), False) + client.variation("other", Context.create("my-org", "organization"), False) + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict( + {"flag.evaluation.hello": True, "flag.evaluation.other": False} + ) diff --git a/tests/integrations/openfeature/test_openfeature.py b/tests/integrations/openfeature/test_openfeature.py index c180211c3f..46acc61ae7 100644 --- a/tests/integrations/openfeature/test_openfeature.py +++ b/tests/integrations/openfeature/test_openfeature.py @@ -7,7 +7,9 @@ from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider import sentry_sdk +from sentry_sdk import start_span, start_transaction from sentry_sdk.integrations.openfeature import OpenFeatureIntegration +from tests.conftest import ApproxDict def test_openfeature_integration(sentry_init, capture_events, uninstall_integration): @@ -151,3 +153,27 @@ async def runner(): {"flag": "world", "result": False}, ] } + + +def test_openfeature_span_integration( + sentry_init, capture_events, uninstall_integration +): + uninstall_integration(OpenFeatureIntegration.identifier) + sentry_init(traces_sample_rate=1.0, integrations=[OpenFeatureIntegration()]) + + api.set_provider( + InMemoryProvider({"hello": InMemoryFlag("on", {"on": True, "off": False})}) + ) + client = api.get_client() + + events = capture_events() + + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + client.get_boolean_value("hello", default_value=False) + client.get_boolean_value("world", default_value=False) + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict( + {"flag.evaluation.hello": True, "flag.evaluation.world": False} + ) diff --git a/tests/integrations/statsig/test_statsig.py b/tests/integrations/statsig/test_statsig.py index c1666bde4d..5eb2cf39f3 100644 --- a/tests/integrations/statsig/test_statsig.py +++ b/tests/integrations/statsig/test_statsig.py @@ -5,6 +5,8 @@ from statsig.statsig_user import StatsigUser from random import random from unittest.mock import Mock +from sentry_sdk import start_span, start_transaction +from tests.conftest import ApproxDict import pytest @@ -181,3 +183,21 @@ def test_wrapper_attributes(sentry_init, uninstall_integration): # Clean up statsig.check_gate = original_check_gate + + +def test_statsig_span_integration(sentry_init, capture_events, uninstall_integration): + uninstall_integration(StatsigIntegration.identifier) + + with mock_statsig({"hello": True}): + sentry_init(traces_sample_rate=1.0, integrations=[StatsigIntegration()]) + events = capture_events() + user = StatsigUser(user_id="user-id") + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + statsig.check_gate(user, "hello") + statsig.check_gate(user, "world") + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict( + {"flag.evaluation.hello": True, "flag.evaluation.world": False} + ) diff --git a/tests/integrations/unleash/test_unleash.py b/tests/integrations/unleash/test_unleash.py index 379abba8f6..98a6188181 100644 --- a/tests/integrations/unleash/test_unleash.py +++ b/tests/integrations/unleash/test_unleash.py @@ -8,7 +8,9 @@ import sentry_sdk from sentry_sdk.integrations.unleash import UnleashIntegration +from sentry_sdk import start_span, start_transaction from tests.integrations.unleash.testutils import mock_unleash_client +from tests.conftest import ApproxDict def test_is_enabled(sentry_init, capture_events, uninstall_integration): @@ -164,3 +166,21 @@ def test_wrapper_attributes(sentry_init, uninstall_integration): # Mock clients methods have not lost their qualified names after decoration. assert client.is_enabled.__name__ == "is_enabled" assert client.is_enabled.__qualname__ == original_is_enabled.__qualname__ + + +def test_unleash_span_integration(sentry_init, capture_events, uninstall_integration): + uninstall_integration(UnleashIntegration.identifier) + + with mock_unleash_client(): + sentry_init(traces_sample_rate=1.0, integrations=[UnleashIntegration()]) + events = capture_events() + client = UnleashClient() # type: ignore[arg-type] + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + client.is_enabled("hello") + client.is_enabled("other") + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict( + {"flag.evaluation.hello": True, "flag.evaluation.other": False} + ) diff --git a/tests/test_feature_flags.py b/tests/test_feature_flags.py index 0df30bd0ea..1b0ed13d49 100644 --- a/tests/test_feature_flags.py +++ b/tests/test_feature_flags.py @@ -7,6 +7,8 @@ import sentry_sdk from sentry_sdk.feature_flags import add_feature_flag, FlagBuffer +from sentry_sdk import start_span, start_transaction +from tests.conftest import ApproxDict def test_featureflags_integration(sentry_init, capture_events, uninstall_integration): @@ -220,3 +222,40 @@ def reader(): # shared resource. When deepcopying we should have exclusive access to the underlying # memory. assert error_occurred is False + + +def test_flag_limit(sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + + events = capture_events() + + with start_transaction(name="hi"): + with start_span(op="foo", name="bar"): + add_feature_flag("0", True) + add_feature_flag("1", True) + add_feature_flag("2", True) + add_feature_flag("3", True) + add_feature_flag("4", True) + add_feature_flag("5", True) + add_feature_flag("6", True) + add_feature_flag("7", True) + add_feature_flag("8", True) + add_feature_flag("9", True) + add_feature_flag("10", True) + + (event,) = events + assert event["spans"][0]["data"] == ApproxDict( + { + "flag.evaluation.0": True, + "flag.evaluation.1": True, + "flag.evaluation.2": True, + "flag.evaluation.3": True, + "flag.evaluation.4": True, + "flag.evaluation.5": True, + "flag.evaluation.6": True, + "flag.evaluation.7": True, + "flag.evaluation.8": True, + "flag.evaluation.9": True, + } + ) + assert "flag.evaluation.10" not in event["spans"][0]["data"] From c3613370f638086bbd4ff235e500e508b1ca877d Mon Sep 17 00:00:00 2001 From: Roman Inflianskas Date: Tue, 22 Apr 2025 12:09:32 +0300 Subject: [PATCH 02/10] test(logs): Avoid failure when running with integrations enabled (#4316) When (at least) one of integrations is enabled (because some dependencies are installed in the environment), `sentry.sdk.name` is changed from `sentry.python` to `sentry.python.[FIRST_ENABLED_INTEGRATION]` which makes `test_logs_attributes` fail. Prevent failure by relaxing the check. This change is beneficial not only for packaging (this patch was required for packaging for Fedora), but also for running tests with `pytest` directly. --- Thank you for contributing to `sentry-python`! Please add tests to validate your changes, and lint your code using `tox -e linters`. Running the test suite on your PR might require maintainer approval. --- tests/test_logs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_logs.py b/tests/test_logs.py index 1c34d52b20..5ede277e3b 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -186,7 +186,7 @@ def test_logs_attributes(sentry_init, capture_envelopes): assert "sentry.release" in logs[0]["attributes"] assert logs[0]["attributes"]["sentry.message.parameters.my_var"] == "some value" assert logs[0]["attributes"][SPANDATA.SERVER_ADDRESS] == "test-server" - assert logs[0]["attributes"]["sentry.sdk.name"] == "sentry.python" + assert logs[0]["attributes"]["sentry.sdk.name"].startswith("sentry.python") assert logs[0]["attributes"]["sentry.sdk.version"] == VERSION From 11e26483d5eeb3f9b35f51e49c69622cd85c88bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Apr 2025 09:14:37 +0000 Subject: [PATCH 03/10] build(deps): bump codecov/codecov-action from 5.4.0 to 5.4.2 (#4318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5.4.0 to 5.4.2.
Release notes

Sourced from codecov/codecov-action's releases.

v5.4.2

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.4.1...v5.4.2

v5.4.1

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.4.0...v5.4.1

v5.4.1-beta

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.4.0...v5.4.1-beta

Changelog

Sourced from codecov/codecov-action's changelog.

v5.4.2

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.4.1..v5.4.2

v5.4.1

What's Changed

Full Changelog: https://github.com/codecov/codecov-action/compare/v5.4.0..v5.4.1

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=5.4.0&new-version=5.4.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Anton Pirker --- .github/workflows/test-integrations-ai.yml | 4 ++-- .github/workflows/test-integrations-cloud.yml | 4 ++-- .github/workflows/test-integrations-common.yml | 2 +- .github/workflows/test-integrations-dbs.yml | 4 ++-- .github/workflows/test-integrations-flags.yml | 2 +- .github/workflows/test-integrations-gevent.yml | 2 +- .github/workflows/test-integrations-graphql.yml | 2 +- .github/workflows/test-integrations-misc.yml | 2 +- .github/workflows/test-integrations-network.yml | 4 ++-- .github/workflows/test-integrations-tasks.yml | 4 ++-- .github/workflows/test-integrations-web-1.yml | 2 +- .github/workflows/test-integrations-web-2.yml | 4 ++-- scripts/split_tox_gh_actions/templates/test_group.jinja | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test-integrations-ai.yml b/.github/workflows/test-integrations-ai.yml index e497ba4280..f392f57f46 100644 --- a/.github/workflows/test-integrations-ai.yml +++ b/.github/workflows/test-integrations-ai.yml @@ -83,7 +83,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -158,7 +158,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-cloud.yml b/.github/workflows/test-integrations-cloud.yml index 1d728f3486..7763aa509d 100644 --- a/.github/workflows/test-integrations-cloud.yml +++ b/.github/workflows/test-integrations-cloud.yml @@ -87,7 +87,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -166,7 +166,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-common.yml b/.github/workflows/test-integrations-common.yml index 4fa12607eb..864583532d 100644 --- a/.github/workflows/test-integrations-common.yml +++ b/.github/workflows/test-integrations-common.yml @@ -67,7 +67,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-dbs.yml b/.github/workflows/test-integrations-dbs.yml index 435ec9d7bb..815b550027 100644 --- a/.github/workflows/test-integrations-dbs.yml +++ b/.github/workflows/test-integrations-dbs.yml @@ -107,7 +107,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -206,7 +206,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-flags.yml b/.github/workflows/test-integrations-flags.yml index f2fdfd5473..e28067841b 100644 --- a/.github/workflows/test-integrations-flags.yml +++ b/.github/workflows/test-integrations-flags.yml @@ -79,7 +79,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-gevent.yml b/.github/workflows/test-integrations-gevent.yml index eb6aa1297f..41a77ffe34 100644 --- a/.github/workflows/test-integrations-gevent.yml +++ b/.github/workflows/test-integrations-gevent.yml @@ -67,7 +67,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-graphql.yml b/.github/workflows/test-integrations-graphql.yml index 9713f80c25..b741302de6 100644 --- a/.github/workflows/test-integrations-graphql.yml +++ b/.github/workflows/test-integrations-graphql.yml @@ -79,7 +79,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-misc.yml b/.github/workflows/test-integrations-misc.yml index 607835ee94..7da9929435 100644 --- a/.github/workflows/test-integrations-misc.yml +++ b/.github/workflows/test-integrations-misc.yml @@ -87,7 +87,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-network.yml b/.github/workflows/test-integrations-network.yml index b51c7bfb07..43b5e4a6a5 100644 --- a/.github/workflows/test-integrations-network.yml +++ b/.github/workflows/test-integrations-network.yml @@ -75,7 +75,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -142,7 +142,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-tasks.yml b/.github/workflows/test-integrations-tasks.yml index a27c13278f..a6850256b2 100644 --- a/.github/workflows/test-integrations-tasks.yml +++ b/.github/workflows/test-integrations-tasks.yml @@ -97,7 +97,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -186,7 +186,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-web-1.yml b/.github/workflows/test-integrations-web-1.yml index ac364ccfc1..b40027ddc7 100644 --- a/.github/workflows/test-integrations-web-1.yml +++ b/.github/workflows/test-integrations-web-1.yml @@ -97,7 +97,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/.github/workflows/test-integrations-web-2.yml b/.github/workflows/test-integrations-web-2.yml index 3d3d6e7c84..1fbff47b65 100644 --- a/.github/workflows/test-integrations-web-2.yml +++ b/.github/workflows/test-integrations-web-2.yml @@ -103,7 +103,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml @@ -198,7 +198,7 @@ jobs: coverage xml - name: Upload coverage to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.xml diff --git a/scripts/split_tox_gh_actions/templates/test_group.jinja b/scripts/split_tox_gh_actions/templates/test_group.jinja index 91849beff4..901e4808e4 100644 --- a/scripts/split_tox_gh_actions/templates/test_group.jinja +++ b/scripts/split_tox_gh_actions/templates/test_group.jinja @@ -91,7 +91,7 @@ - name: Upload coverage to Codecov if: {% raw %}${{ !cancelled() }}{% endraw %} - uses: codecov/codecov-action@v5.4.0 + uses: codecov/codecov-action@v5.4.2 with: token: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} files: coverage.xml From d1819c7786de40bfc322aeab1681715c9dbf05bc Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Tue, 22 Apr 2025 11:17:55 +0200 Subject: [PATCH 04/10] Make all relevant types public (#4315) Make types that users can use when configuring the SDK public. Accompaniyng docs update: https://github.com/getsentry/sentry-docs/pull/13437 Fixes #4127 --- sentry_sdk/_types.py | 6 ++++++ sentry_sdk/types.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/sentry_sdk/_types.py b/sentry_sdk/_types.py index 9bcb5a61f9..7da76e63dc 100644 --- a/sentry_sdk/_types.py +++ b/sentry_sdk/_types.py @@ -220,7 +220,9 @@ class SDKInfo(TypedDict): tuple[None, None, None], ] + # TODO: Make a proper type definition for this (PRs welcome!) Hint = Dict[str, Any] + Log = TypedDict( "Log", { @@ -233,9 +235,13 @@ class SDKInfo(TypedDict): }, ) + # TODO: Make a proper type definition for this (PRs welcome!) Breadcrumb = Dict[str, Any] + + # TODO: Make a proper type definition for this (PRs welcome!) BreadcrumbHint = Dict[str, Any] + # TODO: Make a proper type definition for this (PRs welcome!) SamplingContext = Dict[str, Any] EventProcessor = Callable[[Event, Hint], Optional[Event]] diff --git a/sentry_sdk/types.py b/sentry_sdk/types.py index 2b9f04c097..1a65247584 100644 --- a/sentry_sdk/types.py +++ b/sentry_sdk/types.py @@ -11,15 +11,39 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from sentry_sdk._types import Event, EventDataCategory, Hint, Log + # Re-export types to make them available in the public API + from sentry_sdk._types import ( + Breadcrumb, + BreadcrumbHint, + Event, + EventDataCategory, + Hint, + Log, + MonitorConfig, + SamplingContext, + ) else: from typing import Any # The lines below allow the types to be imported from outside `if TYPE_CHECKING` # guards. The types in this module are only intended to be used for type hints. + Breadcrumb = Any + BreadcrumbHint = Any Event = Any EventDataCategory = Any Hint = Any Log = Any + MonitorConfig = Any + SamplingContext = Any -__all__ = ("Event", "EventDataCategory", "Hint", "Log") + +__all__ = ( + "Breadcrumb", + "BreadcrumbHint", + "Event", + "EventDataCategory", + "Hint", + "Log", + "MonitorConfig", + "SamplingContext", +) From b96e2b64a8fd29d5b55bf419be5c299fc28956e4 Mon Sep 17 00:00:00 2001 From: Dong Guo Date: Tue, 22 Apr 2025 17:27:09 +0800 Subject: [PATCH 05/10] fix(integrations): ASGI integration not capture transactions in Websocket (#4293) In [ASGI Specs](https://github.com/django/asgiref/blob/main/specs/www.rst#websocket-connection-scope), `method` is not in Websocket Connection Scope. --- sentry_sdk/integrations/asgi.py | 25 +++++++++++++------------ tests/integrations/asgi/test_asgi.py | 25 +++++++++++-------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 3569336aae..fc8ee29b1a 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -192,8 +192,8 @@ async def _run_app(self, scope, receive, send, asgi_version): method = scope.get("method", "").upper() transaction = None - if method in self.http_methods_to_capture: - if ty in ("http", "websocket"): + if ty in ("http", "websocket"): + if ty == "websocket" or method in self.http_methods_to_capture: transaction = continue_trace( _get_headers(scope), op="{}.server".format(ty), @@ -205,17 +205,18 @@ async def _run_app(self, scope, receive, send, asgi_version): "[ASGI] Created transaction (continuing trace): %s", transaction, ) - else: - transaction = Transaction( - op=OP.HTTP_SERVER, - name=transaction_name, - source=transaction_source, - origin=self.span_origin, - ) - logger.debug( - "[ASGI] Created transaction (new): %s", transaction - ) + else: + transaction = Transaction( + op=OP.HTTP_SERVER, + name=transaction_name, + source=transaction_source, + origin=self.span_origin, + ) + logger.debug( + "[ASGI] Created transaction (new): %s", transaction + ) + if transaction: transaction.set_tag("asgi.type", ty) logger.debug( "[ASGI] Set transaction name and source on transaction: '%s' / '%s'", diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index f95ea14d01..ec2796c140 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -349,35 +349,32 @@ async def test_trace_from_headers_if_performance_disabled( @pytest.mark.asyncio async def test_websocket(sentry_init, asgi3_ws_app, capture_events, request): - sentry_init(send_default_pii=True) + sentry_init(send_default_pii=True, traces_sample_rate=1.0) events = capture_events() asgi3_ws_app = SentryAsgiMiddleware(asgi3_ws_app) - scope = { - "type": "websocket", - "endpoint": asgi3_app, - "client": ("127.0.0.1", 60457), - "route": "some_url", - "headers": [ - ("accept", "*/*"), - ], - } + request_url = "/ws" with pytest.raises(ValueError): - async with TestClient(asgi3_ws_app, scope=scope) as client: - async with client.websocket_connect("/ws") as ws: - await ws.receive_text() + client = TestClient(asgi3_ws_app) + async with client.websocket_connect(request_url) as ws: + await ws.receive_text() - msg_event, error_event = events + msg_event, error_event, transaction_event = events + assert msg_event["transaction"] == request_url + assert msg_event["transaction_info"] == {"source": "url"} assert msg_event["message"] == "Some message to the world!" (exc,) = error_event["exception"]["values"] assert exc["type"] == "ValueError" assert exc["value"] == "Oh no" + assert transaction_event["transaction"] == request_url + assert transaction_event["transaction_info"] == {"source": "url"} + @pytest.mark.asyncio async def test_auto_session_tracking_with_aggregates( From 434e8afb9762e6eab22165937069271729958d3d Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 23 Apr 2025 10:54:54 +0200 Subject: [PATCH 06/10] tests: Fix version picking in toxgen (#4323) Toxgen should only consider the highest patch release of each `major.minor` version. For the most part this was working fine as long as the releases were ordered as expected in PyPI, but in cases where a lower patch version succeeded a higher patch version in the release list from PyPI, we would incorrectly consider the lower patch version as well, instead of ignoring it in favor of the higher patch. Example: - we pull releases `[1.2.3, 1.2.4, 1.2.5, 1.2.2]` from PyPI (in that order) - we consolidate `1.2.3, 1.2.4, 1.2.5` into one version, `1.2.5`, as expected - `1.2.2` will not disappear into `1.2.5` because of a faulty check in toxgen and will instead be considered as a new version - our resulting list of releases eligible for testing will be `[1.2.5, 1.2.2]` instead of just `[1.2.5]`, which then results in picking versions that are not nicely spaced apart --- scripts/populate_tox/populate_tox.py | 4 +-- tox.ini | 51 ++++++++++++---------------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index 11ea94c0f4..f741496f93 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -190,10 +190,10 @@ def _prefilter_releases( if ( version.major == saved_version.major and version.minor == saved_version.minor - and version.micro > saved_version.micro ): # Don't save all patch versions of a release, just the newest one - filtered_releases[i] = version + if version.micro > saved_version.micro: + filtered_releases[i] = version break else: filtered_releases.append(version) diff --git a/tox.ini b/tox.ini index 9497708ff8..49411b3189 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-04-17T11:01:25.976599+00:00 +# Last generated: 2025-04-23T07:46:44.042662+00:00 [tox] requires = @@ -145,8 +145,8 @@ envlist = # ~~~ AI ~~~ {py3.9,py3.10,py3.11}-cohere-v5.4.0 - {py3.9,py3.11,py3.12}-cohere-v5.9.4 - {py3.9,py3.11,py3.12}-cohere-v5.13.9 + {py3.9,py3.11,py3.12}-cohere-v5.8.1 + {py3.9,py3.11,py3.12}-cohere-v5.11.4 {py3.9,py3.11,py3.12}-cohere-v5.15.0 {py3.8,py3.10,py3.11}-huggingface_hub-v0.22.2 @@ -167,9 +167,8 @@ envlist = {py3.6,py3.7}-redis_py_cluster_legacy-v2.0.0 {py3.6,py3.7,py3.8}-redis_py_cluster_legacy-v2.1.3 - {py3.6,py3.7}-sqlalchemy-v1.3.9 + {py3.6,py3.8,py3.9}-sqlalchemy-v1.3.24 {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.40 @@ -195,7 +194,7 @@ envlist = {py3.8,py3.10,py3.11}-ariadne-v0.20.1 {py3.8,py3.11,py3.12}-ariadne-v0.22 {py3.8,py3.11,py3.12}-ariadne-v0.24.0 - {py3.9,py3.12,py3.13}-ariadne-v0.26.1 + {py3.9,py3.12,py3.13}-ariadne-v0.26.2 {py3.6,py3.9,py3.10}-gql-v3.4.1 {py3.7,py3.11,py3.12}-gql-v3.5.2 @@ -207,7 +206,7 @@ envlist = {py3.8,py3.10,py3.11}-strawberry-v0.209.8 {py3.8,py3.11,py3.12}-strawberry-v0.228.0 {py3.8,py3.12,py3.13}-strawberry-v0.247.2 - {py3.9,py3.12,py3.13}-strawberry-v0.265.1 + {py3.9,py3.12,py3.13}-strawberry-v0.266.0 # ~~~ Network ~~~ @@ -240,12 +239,11 @@ envlist = # ~~~ Web 1 ~~~ - {py3.6}-django-v1.11.9 {py3.6,py3.7}-django-v1.11.29 {py3.6,py3.8,py3.9}-django-v2.2.28 {py3.6,py3.9,py3.10}-django-v3.2.25 {py3.8,py3.11,py3.12}-django-v4.2.20 - {py3.10,py3.11,py3.12}-django-v5.0.9 + {py3.10,py3.11,py3.12}-django-v5.0.14 {py3.10,py3.12,py3.13}-django-v5.2 {py3.6,py3.7,py3.8}-flask-v1.1.4 @@ -266,7 +264,7 @@ envlist = # ~~~ Web 2 ~~~ {py3.6,py3.7}-bottle-v0.12.25 - {py3.6,py3.8,py3.9}-bottle-v0.13.2 + {py3.8,py3.12,py3.13}-bottle-v0.13.3 {py3.6}-falcon-v1.4.1 {py3.6,py3.7}-falcon-v2.0.0 @@ -296,11 +294,11 @@ envlist = # ~~~ Misc ~~~ {py3.6,py3.12,py3.13}-loguru-v0.7.3 - {py3.6}-trytond-v4.6.9 + {py3.6}-trytond-v4.6.22 {py3.6}-trytond-v4.8.18 {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.0.29 {py3.8,py3.11,py3.12}-trytond-v7.4.9 {py3.7,py3.12,py3.13}-typer-v0.15.2 @@ -517,8 +515,8 @@ deps = # ~~~ AI ~~~ cohere-v5.4.0: cohere==5.4.0 - cohere-v5.9.4: cohere==5.9.4 - cohere-v5.13.9: cohere==5.13.9 + cohere-v5.8.1: cohere==5.8.1 + cohere-v5.11.4: cohere==5.11.4 cohere-v5.15.0: cohere==5.15.0 huggingface_hub-v0.22.2: huggingface_hub==0.22.2 @@ -540,9 +538,8 @@ deps = redis_py_cluster_legacy-v2.0.0: redis-py-cluster==2.0.0 redis_py_cluster_legacy-v2.1.3: redis-py-cluster==2.1.3 - sqlalchemy-v1.3.9: sqlalchemy==1.3.9 + sqlalchemy-v1.3.24: sqlalchemy==1.3.24 sqlalchemy-v1.4.54: sqlalchemy==1.4.54 - sqlalchemy-v2.0.9: sqlalchemy==2.0.9 sqlalchemy-v2.0.40: sqlalchemy==2.0.40 @@ -569,7 +566,7 @@ deps = ariadne-v0.20.1: ariadne==0.20.1 ariadne-v0.22: ariadne==0.22 ariadne-v0.24.0: ariadne==0.24.0 - ariadne-v0.26.1: ariadne==0.26.1 + ariadne-v0.26.2: ariadne==0.26.2 ariadne: fastapi ariadne: flask ariadne: httpx @@ -589,7 +586,7 @@ deps = strawberry-v0.209.8: strawberry-graphql[fastapi,flask]==0.209.8 strawberry-v0.228.0: strawberry-graphql[fastapi,flask]==0.228.0 strawberry-v0.247.2: strawberry-graphql[fastapi,flask]==0.247.2 - strawberry-v0.265.1: strawberry-graphql[fastapi,flask]==0.265.1 + strawberry-v0.266.0: strawberry-graphql[fastapi,flask]==0.266.0 strawberry: httpx strawberry-v0.209.8: pydantic<2.11 strawberry-v0.228.0: pydantic<2.11 @@ -633,12 +630,11 @@ deps = # ~~~ Web 1 ~~~ - django-v1.11.9: django==1.11.9 django-v1.11.29: django==1.11.29 django-v2.2.28: django==2.2.28 django-v3.2.25: django==3.2.25 django-v4.2.20: django==4.2.20 - django-v5.0.9: django==5.0.9 + django-v5.0.14: django==5.0.14 django-v5.2: django==5.2 django: psycopg2-binary django: djangorestframework @@ -646,24 +642,21 @@ deps = django: Werkzeug django-v3.2.25: pytest-asyncio django-v4.2.20: pytest-asyncio - django-v5.0.9: pytest-asyncio + django-v5.0.14: pytest-asyncio django-v5.2: pytest-asyncio django-v2.2.28: six - django-v1.11.9: djangorestframework>=3.0,<4.0 - django-v1.11.9: Werkzeug<2.1.0 django-v1.11.29: djangorestframework>=3.0,<4.0 django-v1.11.29: Werkzeug<2.1.0 django-v2.2.28: djangorestframework>=3.0,<4.0 django-v2.2.28: Werkzeug<2.1.0 django-v3.2.25: djangorestframework>=3.0,<4.0 django-v3.2.25: Werkzeug<2.1.0 - django-v1.11.9: pytest-django<4.0 django-v1.11.29: pytest-django<4.0 django-v2.2.28: pytest-django<4.0 django-v2.2.28: channels[daphne] django-v3.2.25: channels[daphne] django-v4.2.20: channels[daphne] - django-v5.0.9: channels[daphne] + django-v5.0.14: channels[daphne] django-v5.2: channels[daphne] flask-v1.1.4: flask==1.1.4 @@ -707,7 +700,7 @@ deps = # ~~~ Web 2 ~~~ bottle-v0.12.25: bottle==0.12.25 - bottle-v0.13.2: bottle==0.13.2 + bottle-v0.13.3: bottle==0.13.3 bottle: werkzeug<2.1.0 falcon-v1.4.1: falcon==1.4.1 @@ -756,14 +749,14 @@ deps = # ~~~ Misc ~~~ loguru-v0.7.3: loguru==0.7.3 - trytond-v4.6.9: trytond==4.6.9 + trytond-v4.6.22: trytond==4.6.22 trytond-v4.8.18: trytond==4.8.18 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.0.29: trytond==7.0.29 trytond-v7.4.9: trytond==7.4.9 trytond: werkzeug - trytond-v4.6.9: werkzeug<1.0 + trytond-v4.6.22: werkzeug<1.0 trytond-v4.8.18: werkzeug<1.0 typer-v0.15.2: typer==0.15.2 From 2c3776c582a23b6936c76ef53008bf63f861b6fd Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 23 Apr 2025 11:03:10 +0200 Subject: [PATCH 07/10] tests: Move aiohttp under toxgen (#4319) Depends on https://github.com/getsentry/sentry-python/pull/4323 --- scripts/populate_tox/config.py | 8 +++++++ scripts/populate_tox/populate_tox.py | 1 - scripts/populate_tox/tox.jinja | 13 ---------- tests/integrations/aiohttp/test_aiohttp.py | 24 ++++++++++++------- tox.ini | 28 +++++++++++----------- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/scripts/populate_tox/config.py b/scripts/populate_tox/config.py index f3f1ba0092..f874ff8a9c 100644 --- a/scripts/populate_tox/config.py +++ b/scripts/populate_tox/config.py @@ -6,6 +6,14 @@ # See scripts/populate_tox/README.md for more info on the format and examples. TEST_SUITE_CONFIG = { + "aiohttp": { + "package": "aiohttp", + "deps": { + "*": ["pytest-aiohttp"], + ">=3.8": ["pytest-asyncio"], + }, + "python": ">=3.7", + }, "ariadne": { "package": "ariadne", "deps": { diff --git a/scripts/populate_tox/populate_tox.py b/scripts/populate_tox/populate_tox.py index f741496f93..c04ab1b209 100644 --- a/scripts/populate_tox/populate_tox.py +++ b/scripts/populate_tox/populate_tox.py @@ -67,7 +67,6 @@ "potel", # Integrations that can be migrated -- we should eventually remove all # of these from the IGNORE list - "aiohttp", "anthropic", "arq", "asyncpg", diff --git a/scripts/populate_tox/tox.jinja b/scripts/populate_tox/tox.jinja index 380a80f690..3cfb5e1252 100644 --- a/scripts/populate_tox/tox.jinja +++ b/scripts/populate_tox/tox.jinja @@ -36,11 +36,6 @@ envlist = # At a minimum, we should test against at least the lowest # and the latest supported version of a framework. - # AIOHTTP - {py3.7}-aiohttp-v{3.4} - {py3.7,py3.9,py3.11}-aiohttp-v{3.8} - {py3.8,py3.12,py3.13}-aiohttp-latest - # Anthropic {py3.8,py3.11,py3.12}-anthropic-v{0.16,0.28,0.40} {py3.7,py3.11,py3.12}-anthropic-latest @@ -184,14 +179,6 @@ deps = # === Integrations === - # AIOHTTP - aiohttp-v3.4: aiohttp~=3.4.0 - aiohttp-v3.8: aiohttp~=3.8.0 - aiohttp-latest: aiohttp - aiohttp: pytest-aiohttp - aiohttp-v3.8: pytest-asyncio - aiohttp-latest: pytest-asyncio - # Anthropic anthropic: pytest-asyncio anthropic-v{0.16,0.28}: httpx<0.28.0 diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py index ef7c04e90a..06859b127f 100644 --- a/tests/integrations/aiohttp/test_aiohttp.py +++ b/tests/integrations/aiohttp/test_aiohttp.py @@ -1,10 +1,16 @@ import asyncio import json -import sys + from contextlib import suppress from unittest import mock import pytest + +try: + import pytest_asyncio +except ImportError: + pytest_asyncio = None + from aiohttp import web, ClientSession from aiohttp.client import ServerDisconnectedError from aiohttp.web_request import Request @@ -21,6 +27,14 @@ from tests.conftest import ApproxDict +if pytest_asyncio is None: + # `loop` was deprecated in `pytest-aiohttp` + # in favor of `event_loop` from `pytest-asyncio` + @pytest.fixture + def event_loop(loop): + yield loop + + @pytest.mark.asyncio async def test_basic(sentry_init, aiohttp_client, capture_events): sentry_init(integrations=[AioHttpIntegration()]) @@ -474,14 +488,6 @@ async def hello(request): assert error_event["contexts"]["trace"]["trace_id"] == trace_id -if sys.version_info < (3, 12): - # `loop` was deprecated in `pytest-aiohttp` - # in favor of `event_loop` from `pytest-asyncio` - @pytest.fixture - def event_loop(loop): - yield loop - - @pytest.mark.asyncio async def test_crumb_capture( sentry_init, aiohttp_raw_server, aiohttp_client, event_loop, capture_events diff --git a/tox.ini b/tox.ini index 49411b3189..6f3b9863e8 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-04-23T07:46:44.042662+00:00 +# Last generated: 2025-04-23T08:07:00.653648+00:00 [tox] requires = @@ -36,11 +36,6 @@ envlist = # At a minimum, we should test against at least the lowest # and the latest supported version of a framework. - # AIOHTTP - {py3.7}-aiohttp-v{3.4} - {py3.7,py3.9,py3.11}-aiohttp-v{3.8} - {py3.8,py3.12,py3.13}-aiohttp-latest - # Anthropic {py3.8,py3.11,py3.12}-anthropic-v{0.16,0.28,0.40} {py3.7,py3.11,py3.12}-anthropic-latest @@ -263,6 +258,11 @@ envlist = # ~~~ Web 2 ~~~ + {py3.7}-aiohttp-v3.4.4 + {py3.7}-aiohttp-v3.6.3 + {py3.7,py3.9,py3.10}-aiohttp-v3.8.6 + {py3.9,py3.12,py3.13}-aiohttp-v3.11.18 + {py3.6,py3.7}-bottle-v0.12.25 {py3.8,py3.12,py3.13}-bottle-v0.13.3 @@ -335,14 +335,6 @@ deps = # === Integrations === - # AIOHTTP - aiohttp-v3.4: aiohttp~=3.4.0 - aiohttp-v3.8: aiohttp~=3.8.0 - aiohttp-latest: aiohttp - aiohttp: pytest-aiohttp - aiohttp-v3.8: pytest-asyncio - aiohttp-latest: pytest-asyncio - # Anthropic anthropic: pytest-asyncio anthropic-v{0.16,0.28}: httpx<0.28.0 @@ -699,6 +691,14 @@ deps = # ~~~ Web 2 ~~~ + aiohttp-v3.4.4: aiohttp==3.4.4 + aiohttp-v3.6.3: aiohttp==3.6.3 + aiohttp-v3.8.6: aiohttp==3.8.6 + aiohttp-v3.11.18: aiohttp==3.11.18 + aiohttp: pytest-aiohttp + aiohttp-v3.8.6: pytest-asyncio + aiohttp-v3.11.18: pytest-asyncio + bottle-v0.12.25: bottle==0.12.25 bottle-v0.13.3: bottle==0.13.3 bottle: werkzeug<2.1.0 From bbb41a31a71e90b3a72ded603ca0cd9173e23522 Mon Sep 17 00:00:00 2001 From: Anton Pirker Date: Wed, 23 Apr 2025 15:06:32 +0200 Subject: [PATCH 08/10] Make sure to use the default decimal context in our code (#4231) Fixes #4213 --- sentry_sdk/tracing.py | 7 +++---- sentry_sdk/tracing_utils.py | 13 ++++++++----- tests/tracing/test_sample_rand.py | 10 +++++++++- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index ae0b90253e..ca249fe8fe 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1,3 +1,4 @@ +from decimal import Decimal import uuid import warnings from datetime import datetime, timedelta, timezone @@ -1198,10 +1199,8 @@ def _set_initial_sampling_decision(self, sampling_context): self.sampled = False return - # Now we roll the dice. self._sample_rand is inclusive of 0, but not of 1, - # so strict < is safe here. In case sample_rate is a boolean, cast it - # to a float (True becomes 1.0 and False becomes 0.0) - self.sampled = self._sample_rand < self.sample_rate + # Now we roll the dice. + self.sampled = self._sample_rand < Decimal.from_float(self.sample_rate) if self.sampled: logger.debug( diff --git a/sentry_sdk/tracing_utils.py b/sentry_sdk/tracing_utils.py index ba56695740..552f4fd59a 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, Context, Decimal +from decimal import ROUND_DOWN, Decimal, DefaultContext, localcontext from functools import wraps from random import Random from urllib.parse import quote, unquote @@ -872,10 +872,13 @@ def _generate_sample_rand( # Round down to exactly six decimal-digit precision. # 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) - ) + # in case the user has changed the default precision or set traps. + with localcontext(DefaultContext) as ctx: + ctx.prec = 6 + return Decimal(sample_rand).quantize( + Decimal("0.000001"), + rounding=ROUND_DOWN, + ) 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 ef277a3dec..f9c10aa04e 100644 --- a/tests/tracing/test_sample_rand.py +++ b/tests/tracing/test_sample_rand.py @@ -1,4 +1,5 @@ import decimal +from decimal import Inexact, FloatOperation from unittest import mock import pytest @@ -58,14 +59,19 @@ def test_transaction_uses_incoming_sample_rand( def test_decimal_context(sentry_init, capture_events): """ - Ensure that having a decimal context with a precision below 6 + Ensure that having a user altered 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 + old_inexact = decimal.getcontext().traps[Inexact] + old_float_operation = decimal.getcontext().traps[FloatOperation] + decimal.getcontext().prec = 2 + decimal.getcontext().traps[Inexact] = True + decimal.getcontext().traps[FloatOperation] = True try: with mock.patch( @@ -77,5 +83,7 @@ def test_decimal_context(sentry_init, capture_events): ) finally: decimal.getcontext().prec = old_prec + decimal.getcontext().traps[Inexact] = old_inexact + decimal.getcontext().traps[FloatOperation] = old_float_operation assert len(events) == 1 From 049f2a0b18e22be7b5e77eb31b11122f2a38c92a Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 24 Apr 2025 08:02:13 +0000 Subject: [PATCH 09/10] release: 2.27.0 --- CHANGELOG.md | 19 +++++++++++++++++++ docs/conf.py | 2 +- sentry_sdk/consts.py | 2 +- setup.py | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb49ed54ca..70915e75c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 2.27.0 + +### Various fixes & improvements + +- Make sure to use the default decimal context in our code (#4231) by @antonpirker +- tests: Move aiohttp under toxgen (#4319) by @sentrivana +- tests: Fix version picking in toxgen (#4323) by @sentrivana +- fix(integrations): ASGI integration not capture transactions in Websocket (#4293) by @guodong000 +- Make all relevant types public (#4315) by @antonpirker +- build(deps): bump codecov/codecov-action from 5.4.0 to 5.4.2 (#4318) by @dependabot +- test(logs): Avoid failure when running with integrations enabled (#4316) by @rominf +- feat(spans): Record flag evaluations as span attributes (#4280) by @cmanallen +- toxgen: Remove unused code and rerun (#4313) by @sentrivana +- toxgen: Add cohere (#4304) by @sentrivana +- toxgen: Migrate fastapi (#4302) by @sentrivana +- toxgen: Add huggingface_hub (#4299) by @sentrivana +- toxgen: Add huey (#4298) by @sentrivana +- tests: Update tox.ini (#4297) by @sentrivana + ## 2.26.1 ### Various fixes & improvements diff --git a/docs/conf.py b/docs/conf.py index 629b5b9eaa..709f557d16 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.26.1" +release = "2.27.0" version = ".".join(release.split(".")[:2]) # The short X.Y version. diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 3802980b82..e1f18fe4ae 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -966,4 +966,4 @@ def _get_default_options(): del _get_default_options -VERSION = "2.26.1" +VERSION = "2.27.0" diff --git a/setup.py b/setup.py index 62f4867b35..877585472b 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ def get_file_text(file_name): setup( name="sentry-sdk", - version="2.26.1", + version="2.27.0", author="Sentry Team and Contributors", author_email="hello@sentry.io", url="https://github.com/getsentry/sentry-python", From 919bdeab17dff035131b0f70848d5675efd96808 Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Thu, 24 Apr 2025 10:04:12 +0200 Subject: [PATCH 10/10] Update CHANGELOG.md --- CHANGELOG.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70915e75c5..786a9a34e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,20 +4,20 @@ ### Various fixes & improvements -- Make sure to use the default decimal context in our code (#4231) by @antonpirker -- tests: Move aiohttp under toxgen (#4319) by @sentrivana -- tests: Fix version picking in toxgen (#4323) by @sentrivana +- fix: Make sure to use the default decimal context in our code (#4231) by @antonpirker - fix(integrations): ASGI integration not capture transactions in Websocket (#4293) by @guodong000 -- Make all relevant types public (#4315) by @antonpirker -- build(deps): bump codecov/codecov-action from 5.4.0 to 5.4.2 (#4318) by @dependabot -- test(logs): Avoid failure when running with integrations enabled (#4316) by @rominf +- feat(typing): Make all relevant types public (#4315) by @antonpirker - feat(spans): Record flag evaluations as span attributes (#4280) by @cmanallen -- toxgen: Remove unused code and rerun (#4313) by @sentrivana -- toxgen: Add cohere (#4304) by @sentrivana -- toxgen: Migrate fastapi (#4302) by @sentrivana -- toxgen: Add huggingface_hub (#4299) by @sentrivana -- toxgen: Add huey (#4298) by @sentrivana +- test(logs): Avoid failure when running with integrations enabled (#4316) by @rominf +- tests: Remove unused code and rerun (#4313) by @sentrivana +- tests: Add cohere to toxgen (#4304) by @sentrivana +- tests: Migrate fastapi to toxgen (#4302) by @sentrivana +- tests: Add huggingface_hub to toxgen (#4299) by @sentrivana +- tests: Add huey to toxgen (#4298) by @sentrivana - tests: Update tox.ini (#4297) by @sentrivana +- tests: Move aiohttp under toxgen (#4319) by @sentrivana +- tests: Fix version picking in toxgen (#4323) by @sentrivana +- build(deps): bump codecov/codecov-action from 5.4.0 to 5.4.2 (#4318) by @dependabot ## 2.26.1