Skip to content

Commit bab41ae

Browse files
authored
Cap sentry-sdk below 3 and remove the 3.x code paths (#379)
1 parent f9ee273 commit bab41ae

12 files changed

Lines changed: 78 additions & 160 deletions

File tree

‎.github/workflows/_checks.yml‎

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,6 @@ jobs:
6565
- run: uv python pin ${{ matrix.python-version }}
6666
- run: just install
6767
- run: just test-ci
68-
sentry-majors:
69-
# The pytest jobs resolve whichever sentry-sdk major is newest; this runs the suite on each one.
70-
runs-on: ubuntu-latest
71-
strategy:
72-
fail-fast: false
73-
matrix:
74-
sentry-sdk: ["sentry-sdk<3", "sentry-sdk==3.0.0b2"]
75-
steps:
76-
- uses: actions/checkout@v6
77-
- uses: extractions/setup-just@v4
78-
- uses: astral-sh/setup-uv@v8.2.0
79-
with:
80-
enable-cache: true
81-
cache-dependency-glob: "**/pyproject.toml"
82-
- run: uv python install 3.14
83-
- run: uv python pin 3.14
84-
- run: just install
85-
- run: uv pip install "${{ matrix.sentry-sdk }}"
86-
- run: just test
8768
free-threaded:
8869
runs-on: ubuntu-latest
8970
strategy:

‎benchmarks/README.md‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -721,11 +721,7 @@ scenario there, `full_all_tuned` included, is reachable through `FastAPIConfig`.
721721
[sentry-python#7400](https://github.com/getsentry/sentry-python/issues/7400) and imports nothing
722722
from this directory.
723723

724-
Every number here was measured on sentry-sdk 2.67.1. The scripts also run on 3.x, which builds no
725-
transaction while tracing is off, so the patched scenarios, `errors_only_txn_endpoint_style` and
726-
`repro_sentry_txn.py --patched` exist only on 2.x. On 3.x, `micro.py` times `continue_trace` and
727-
the root `start_span` in place of the `Transaction` calls. 3.x also leaves `LoggingIntegration` out
728-
of its default integrations, so its `errors_only` records no log breadcrumbs.
724+
Every number here was measured on sentry-sdk 2.67.1. The scripts run on sentry-sdk 2.x only.
729725

730726
Numbers are machine-specific and move a few percent run to run; the ratios and the ordering are
731727
the durable part. Everything here was measured in one session on one idle machine, which is the

‎benchmarks/micro.py‎

Lines changed: 14 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
"""Per-operation costs of the pieces sentry-sdk runs on every request.
22
33
Attributes the per-request total from `run.py` to individual calls, which is what makes the
4-
upstream issues actionable: on sentry-sdk 2.x, `Transaction(...)` and the Mersenne Twister seed
5-
inside it are the two largest items, and neither is reachable from a configuration knob. 3.x builds
6-
no transaction while tracing is off, so there it times `continue_trace` and the root `start_span`
7-
that the ASGI integration calls on every request instead.
4+
upstream issues actionable: `Transaction(...)` and the Mersenne Twister seed inside it are the
5+
two largest items, and neither is reachable from a configuration knob.
86
"""
97

10-
import importlib
118
import statistics
129
import timeit
1310
import typing
@@ -17,10 +14,9 @@
1714
import sentry_scenarios
1815
import sentry_sdk
1916
from sentry_sdk.scope import Scope
17+
from sentry_sdk.tracing import Transaction
2018
from sentry_sdk.tracing_utils import Baggage, PropagationContext, _generate_sample_rand
2119

22-
from lite_bootstrap.instruments.sentry_instrument import IS_SENTRY_SDK_2
23-
2420

2521
HEADERS: typing.Final = {
2622
"host": "testserver",
@@ -29,7 +25,6 @@
2925
"connection": "keep-alive",
3026
}
3127
TRACE_ID: typing.Final = uuid.uuid4().hex
32-
SDK2_TRACING: typing.Final[typing.Any] = importlib.import_module("sentry_sdk.tracing") if IS_SENTRY_SDK_2 else None
3328

3429

3530
def timed(label: str, stmt: typing.Callable[[], object], number: int = 20000, repeat: int = 5) -> None:
@@ -39,35 +34,15 @@ def timed(label: str, stmt: typing.Callable[[], object], number: int = 20000, re
3934
print(f"{label:<46} {best:>8.2f} us (best) {median:>8.2f} us (med)")
4035

4136

42-
def time_sdk2_transaction(isolation_scope: typing.Any, current_scope: typing.Any) -> None: # noqa: ANN401
43-
def new_transaction() -> object:
44-
return SDK2_TRACING.Transaction(op="http.server", name="GET /ping", source="route")
45-
46-
def start_and_finish() -> None:
47-
with current_scope.start_transaction(new_transaction(), custom_sampling_context={"asgi_scope": {}}):
48-
pass
49-
50-
timed(
51-
"scope.continue_trace(headers)",
52-
lambda: isolation_scope.continue_trace(HEADERS, op="http.server", name="GET /ping", source="route"),
53-
)
54-
timed("Transaction(op, name, source)", new_transaction)
55-
timed("start_transaction(txn) + exit (tracing off)", start_and_finish, number=5000)
56-
57-
58-
def time_sdk3_span() -> None:
59-
def start_and_end() -> None:
60-
with sentry_sdk.start_span(name="GET /ping", parent_span=None):
61-
pass
62-
63-
timed("continue_trace(headers)", lambda: sentry_sdk.continue_trace(HEADERS))
64-
timed("start_span(root) + exit (tracing off)", start_and_end, number=5000)
65-
66-
6737
def main() -> None:
6838
sentry_sdk.init(**sentry_scenarios.base_kwargs(max_breadcrumbs=15, attach_stacktrace=True))
6939
scope = Scope.get_isolation_scope()
7040

41+
def start_and_finish() -> None:
42+
transaction = Transaction(op="http.server", name="GET /ping", source="route")
43+
with sentry_sdk.start_transaction(transaction, custom_sampling_context={"asgi_scope": {}}):
44+
pass
45+
7146
def isolation() -> None:
7247
with sentry_sdk.isolation_scope():
7348
pass
@@ -79,10 +54,12 @@ def isolation() -> None:
7954
timed("PropagationContext.from_incoming_data(headers)", lambda: PropagationContext.from_incoming_data(HEADERS))
8055
timed("Baggage.from_incoming_header(None)", lambda: Baggage.from_incoming_header(None))
8156
timed("scope.generate_propagation_context(headers)", lambda: scope.generate_propagation_context(HEADERS))
82-
if IS_SENTRY_SDK_2:
83-
time_sdk2_transaction(scope, sentry_sdk.get_current_scope())
84-
else:
85-
time_sdk3_span()
57+
timed(
58+
"scope.continue_trace(headers)",
59+
lambda: scope.continue_trace(HEADERS, op="http.server", name="GET /ping", source="route"),
60+
)
61+
timed("Transaction(op, name, source)", lambda: Transaction(op="http.server", name="GET /ping", source="route"))
62+
timed("start_transaction(txn) + exit (tracing off)", start_and_finish, number=5000)
8663
timed("isolation_scope() enter/exit", isolation)
8764
timed("scope.fork()", scope.fork)
8865
timed("get_client()", sentry_sdk.get_client)

‎benchmarks/repro_sentry_txn.py‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
python repro_sentry_txn.py --off # SDK never initialised
88
python repro_sentry_txn.py # SDK as shipped
99
python repro_sentry_txn.py --patched # skip the Transaction, keep trace propagation
10-
11-
`--patched` runs on sentry-sdk 2.x only: 3.x builds no transaction while tracing is off.
1210
"""
1311

1412
import asyncio
@@ -57,7 +55,7 @@ def setup(*, patched: bool) -> None:
5755
# tracing is DISABLED: traces_sample_rate and traces_sampler are both unset
5856
sentry_sdk.init(dsn="https://public@o0.ingest.sentry.io/0", transport=NullTransport)
5957
if patched:
60-
setattr(Scope, "continue_trace", continue_trace_without_transaction) # noqa: B010
58+
Scope.continue_trace = continue_trace_without_transaction # ty: ignore[invalid-assignment]
6159

6260

6361
async def drive(app: typing.Callable[..., typing.Awaitable[None]], requests: int) -> float:
@@ -78,8 +76,6 @@ async def send(message: dict[str, typing.Any]) -> None:
7876

7977
def main() -> None:
8078
if "--patched" in sys.argv:
81-
if not sentry_sdk.VERSION.startswith("2."):
82-
sys.exit(f"--patched needs sentry-sdk 2.x; {sentry_sdk.VERSION} builds no transaction to skip")
8379
mode = "patched"
8480
elif "--off" in sys.argv:
8581
mode = "off"

‎benchmarks/sentry_scenarios.py‎

Lines changed: 25 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@
55
can be undone.
66
77
The patched scenarios are not configurations a user can reach - they simulate an upstream
8-
fix so its value can be measured before proposing it. They, and `errors_only_txn_endpoint_style`,
9-
exist only on sentry-sdk 2.x: 3.x builds no transaction while tracing is off and has no
10-
`transaction_style`.
8+
fix so its value can be measured before proposing it.
119
"""
1210

13-
import importlib
1411
import logging
1512
import typing
1613

1714
import sentry_sdk
15+
import sentry_sdk.tracing
1816
from driver import DSN, TRANSPORT
1917
from fastapi import FastAPI
2018
from sentry_sdk.integrations.dedupe import DedupeIntegration
@@ -27,10 +25,6 @@
2725
from sentry_sdk.integrations.threading import ThreadingIntegration
2826
from sentry_sdk.scope import Scope
2927

30-
from lite_bootstrap.instruments.sentry_instrument import IS_SENTRY_SDK_2
31-
32-
33-
SDK2_TRACING: typing.Final = importlib.import_module("sentry_sdk.tracing") if IS_SENTRY_SDK_2 else None
3428

3529
logger = logging.getLogger("bench")
3630

@@ -66,14 +60,8 @@ def _no_transaction_integrations() -> list[typing.Any]:
6660

6761

6862
def _errors_and_breadcrumbs_only(*, breadcrumbs: bool) -> LoggingIntegration:
69-
"""Errors become events and nothing goes to Sentry Logs; 3.0 renamed the arguments for that."""
70-
breadcrumb_level = logging.INFO if breadcrumbs else None
71-
kwargs: dict[str, typing.Any] = (
72-
{"level": breadcrumb_level, "sentry_logs_level": None}
73-
if IS_SENTRY_SDK_2
74-
else {"breadcrumb_level": breadcrumb_level, "event_level": logging.ERROR, "level": None}
75-
)
76-
return LoggingIntegration(**kwargs)
63+
"""Errors become events and nothing goes to Sentry Logs."""
64+
return LoggingIntegration(level=logging.INFO if breadcrumbs else None, sentry_logs_level=None)
7765

7866

7967
SCENARIOS: dict[str, typing.Callable[[], dict[str, typing.Any]] | None] = {
@@ -155,7 +143,7 @@ def _errors_and_breadcrumbs_only(*, breadcrumbs: bool) -> LoggingIntegration:
155143

156144
def _patch_lazy_sample_rand() -> None:
157145
"""`Transaction.__init__` derives sample_rand from the trace id even when tracing is off."""
158-
setattr(SDK2_TRACING, "_generate_sample_rand", lambda trace_id, **kwargs: 0.5) # noqa: ARG005, B010
146+
sentry_sdk.tracing._generate_sample_rand = lambda trace_id, **kwargs: 0.5 # ty: ignore[invalid-assignment] # noqa: ARG005
159147

160148

161149
def _patch_skip_transaction() -> None:
@@ -166,30 +154,29 @@ def continue_trace(
166154
) -> None:
167155
self.generate_propagation_context(environ_or_headers)
168156

169-
setattr(Scope, "continue_trace", continue_trace) # noqa: B010
157+
Scope.continue_trace = continue_trace # ty: ignore[invalid-assignment]
170158

171159

172-
PATCHES: dict[str, list[typing.Callable[[], None]]] = {}
160+
SCENARIOS["errors_only_txn_endpoint_style"] = lambda: base_kwargs(
161+
max_breadcrumbs=15,
162+
attach_stacktrace=True,
163+
integrations=[
164+
StarletteIntegration(transaction_style="endpoint"),
165+
FastApiIntegration(transaction_style="endpoint"),
166+
],
167+
)
173168

174-
if IS_SENTRY_SDK_2:
175-
endpoint_style: dict[str, typing.Any] = {"transaction_style": "endpoint"}
176-
SCENARIOS["errors_only_txn_endpoint_style"] = lambda: base_kwargs(
177-
max_breadcrumbs=15,
178-
attach_stacktrace=True,
179-
integrations=[StarletteIntegration(**endpoint_style), FastApiIntegration(**endpoint_style)],
180-
)
181-
PATCHES.update(
182-
{
183-
"errors_only_lazy_sample_rand": [_patch_lazy_sample_rand],
184-
"errors_only_skip_txn": [_patch_skip_transaction],
185-
"errors_only_skip_txn_no_sessions": [_patch_skip_transaction],
186-
"errors_only_logging_lean_skip_txn": [_patch_skip_transaction],
187-
}
188-
)
189-
SCENARIOS["errors_only_lazy_sample_rand"] = SCENARIOS["errors_only"]
190-
SCENARIOS["errors_only_skip_txn"] = SCENARIOS["errors_only"]
191-
SCENARIOS["errors_only_skip_txn_no_sessions"] = SCENARIOS["errors_only_no_sessions"]
192-
SCENARIOS["errors_only_logging_lean_skip_txn"] = SCENARIOS["errors_only_logging_lean"]
169+
PATCHES: dict[str, list[typing.Callable[[], None]]] = {
170+
"errors_only_lazy_sample_rand": [_patch_lazy_sample_rand],
171+
"errors_only_skip_txn": [_patch_skip_transaction],
172+
"errors_only_skip_txn_no_sessions": [_patch_skip_transaction],
173+
"errors_only_logging_lean_skip_txn": [_patch_skip_transaction],
174+
}
175+
176+
SCENARIOS["errors_only_lazy_sample_rand"] = SCENARIOS["errors_only"]
177+
SCENARIOS["errors_only_skip_txn"] = SCENARIOS["errors_only"]
178+
SCENARIOS["errors_only_skip_txn_no_sessions"] = SCENARIOS["errors_only_no_sessions"]
179+
SCENARIOS["errors_only_logging_lean_skip_txn"] = SCENARIOS["errors_only_logging_lean"]
193180

194181

195182
def setup(scenario: str) -> None:

‎docs/integrations/fastapi.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,12 @@ event either way.
9494

9595
Measured in-process on a do-nothing handler, over the same app without Sentry:
9696

97-
| Sentry setup | sentry-sdk 2.73.0 | 3.0.0b2 |
98-
|---|---:|---:|
99-
| lean capture, the default | +6.1 µs | +5.1 µs |
100-
| the same, request carries a sampled `sentry-trace` | +11.5 µs | +10.9 µs |
101-
| `sentry_lean_error_capture=False` | +57.1 µs | +47.0 µs |
102-
| the same, request carries a sampled `sentry-trace` | +59.2 µs | +113.2 µs |
97+
| Sentry setup | sentry-sdk 2.73.0 |
98+
|---|---:|
99+
| lean capture, the default | +6.1 µs |
100+
| the same, request carries a sampled `sentry-trace` | +11.5 µs |
101+
| `sentry_lean_error_capture=False` | +57.1 µs |
102+
| the same, request carries a sampled `sentry-trace` | +59.2 µs |
103103

104104
sentry-sdk records installed integrations for the whole process, and `disabled_integrations`
105105
cannot remove one that an earlier `sentry_sdk.init` installed. That matters only with

‎docs/integrations/fastmcp.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ attached, with lean capture on or off and with `MCPIntegration` on or off.
8585

8686
Measured in-process per `tools/call` over stateless JSON HTTP, over the same app without Sentry:
8787

88-
| Sentry setup | sentry-sdk 2.73.0 | 3.0.0b2 |
89-
|---|---:|---:|
90-
| lean capture, the default | +20.6 µs | +15.9 µs |
91-
| `sentry_lean_error_capture=False` | +82.2 µs | +65.0 µs |
92-
| the same, request carries a sampled `sentry-trace` | +82.3 µs | +216.4 µs |
88+
| Sentry setup | sentry-sdk 2.73.0 |
89+
|---|---:|
90+
| lean capture, the default | +20.6 µs |
91+
| `sentry_lean_error_capture=False` | +82.2 µs |
92+
| the same, request carries a sampled `sentry-trace` | +82.3 µs |
9393

9494
A tool call costs about 620 µs before Sentry, and rows differ by up to 15% between runs.
9595

‎docs/integrations/litestar.md‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,7 @@ sentry-sdk's `LitestarIntegration`. Tracing counts as off when `sentry_traces_sa
113113
and `sentry_additional_params` sets none of `traces_sample_rate`, `traces_sampler` and
114114
`enable_tracing`. With tracing off, the integration still builds a transaction and a span per
115115
middleware for every request and then discards them
116-
([getsentry/sentry-python#7400](https://github.com/getsentry/sentry-python/issues/7400)). On
117-
sentry-sdk 3.0, an incoming `sentry-trace` header marked as sampled makes it record a real span.
116+
([getsentry/sentry-python#7400](https://github.com/getsentry/sentry-python/issues/7400)).
118117

119118
`sentry_lean_error_capture` picks the path:
120119

@@ -143,11 +142,11 @@ answer with a 404 is not reported. The integration reports every exception excep
143142

144143
Measured in-process on a do-nothing handler, over the same app without Sentry:
145144

146-
| Sentry setup | sentry-sdk 2.73.0 | 3.0.0b2 |
147-
|---|---:|---:|
148-
| lean capture, the default | +7.3 µs | +8.3 µs |
149-
| `sentry_lean_error_capture=False` | +70.7 µs | +59.5 µs |
150-
| the same, request carries a sampled `sentry-trace` | +74.4 µs | +147.4 µs |
145+
| Sentry setup | sentry-sdk 2.73.0 |
146+
|---|---:|
147+
| lean capture, the default | +7.3 µs |
148+
| `sentry_lean_error_capture=False` | +70.7 µs |
149+
| the same, request carries a sampled `sentry-trace` | +74.4 µs |
151150

152151
sentry-sdk records installed integrations for the whole process, and `disabled_integrations`
153152
cannot remove one that an earlier `sentry_sdk.init` installed. That matters only with

‎docs/introduction/configuration.md‎

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,13 @@ Litestar, FastAPI and FastMCP while Sentry tracing is off; see [Performance](per
5151

5252
### Sentry logging integration
5353

54-
lite-bootstrap supports sentry-sdk 2.70 and later, on both the 2.x and 3.x majors. Unless
55-
`sentry_integrations` already contains a `LoggingIntegration`, it appends one that turns `ERROR`
56-
records into events, records breadcrumbs from `sentry_logging_breadcrumb_level` up and sends no
57-
Sentry Logs. 3.0 renamed the integration's arguments, so the call differs per major:
58-
59-
- 2.x: `LoggingIntegration(level=sentry_logging_breadcrumb_level, sentry_logs_level=None)`, with the
60-
event level left at its `ERROR` default.
61-
- 3.x: `LoggingIntegration(breadcrumb_level=sentry_logging_breadcrumb_level, event_level=logging.ERROR, level=None)`.
62-
On 3.x `level` means Sentry Logs, and `event_level` defaults to `None`, so without it a logged
63-
error creates no event.
64-
65-
On 2.x, turning `sentry_logs_level` off is free. lite-bootstrap never sets `enable_logs`, so Sentry
54+
lite-bootstrap supports sentry-sdk 2.70 and later 2.x releases. sentry-sdk 3.0 is not supported
55+
yet. Unless `sentry_integrations` already contains a `LoggingIntegration`, lite-bootstrap appends
56+
`LoggingIntegration(level=sentry_logging_breadcrumb_level, sentry_logs_level=None)`. It turns
57+
`ERROR` records into events, records breadcrumbs from `sentry_logging_breadcrumb_level` up and sends
58+
no Sentry Logs.
59+
60+
Turning `sentry_logs_level` off is free. lite-bootstrap never sets `enable_logs`, so Sentry
6661
Logs is off, but `SentryLogsHandler.emit` formats the record *before* it checks whether logs are
6762
enabled ([getsentry/sentry-python#7402](https://github.com/getsentry/sentry-python/issues/7402)) - it
6863
formats every `INFO`+ record and discards the result.
@@ -92,8 +87,8 @@ SQLAlchemy, asyncpg, redis, httpx, aiohttp, MCP and others. lite-bootstrap passe
9287

9388
With tracing off, those integrations still open a span and record a breadcrumb for every SQL
9489
statement, Redis command or outgoing HTTP request. `SqlalchemyIntegration` measured about 64 µs per
95-
statement on sentry-sdk 2.70, and `RedisIntegration` +27.1 µs per FastStream publish on 2.73.0 and
96-
+9.5 µs on 3.0.0b2 ([FastStream](https://github.com/modern-python/lite-bootstrap/blob/main/benchmarks/README.md#8d-faststream)). The default integrations stay:
90+
statement on sentry-sdk 2.70, and `RedisIntegration` +27.1 µs per FastStream publish on 2.73.0
91+
([FastStream](https://github.com/modern-python/lite-bootstrap/blob/main/benchmarks/README.md#8d-faststream)). The default integrations stay:
9792
dedupe, excepthook, threading, stdlib, atexit, modules, argv and lite-bootstrap's own
9893
`LoggingIntegration`.
9994

@@ -116,12 +111,8 @@ default as a whole, so keys you leave out take sentry-sdk's own defaults, which
116111
change one key, spread the default: `{**DEFAULT_DATA_COLLECTION, "user_info": True}`. Read more about
117112
the option [here](https://docs.sentry.io/platforms/python/data-management/data-collected/).
118113

119-
`send_default_pii` has no effect on 2.x while `data_collection` is set, and on 3.x it is gone. 3.0
120-
also raises `TypeError` at `sentry_sdk.init` for any option it removed, so passing one of these
121-
through `sentry_additional_params` fails at bootstrap on 3.x, `enable_tracing`, `enable_logs` and
122-
`include_local_variables` among them. sentry-sdk's
123-
[migration guide](https://github.com/getsentry/sentry-python/blob/master/MIGRATION_GUIDE.md) lists
124-
every removed option.
114+
`send_default_pii` has no effect while `data_collection` is set, because sentry-sdk lets
115+
`data_collection` take precedence.
125116

126117

127118
## Prometheus

0 commit comments

Comments
 (0)