Skip to content

Commit 03a20c0

Browse files
authored
feat(sentry)!: support sentry-sdk 3 alongside 2.x and raise the floor to 2.70 (#359)
* feat(sentry)!: support sentry-sdk 3 alongside 2.x and raise the floor to 2.70 * chore(benchmarks): run on sentry-sdk 3 and keep the 2.x transaction probes 2.x-only
1 parent 4e80f17 commit 03a20c0

9 files changed

Lines changed: 228 additions & 132 deletions

File tree

‎.github/workflows/_checks.yml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,25 @@ 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
6887
free-threaded:
6988
runs-on: ubuntu-latest
7089
strategy:

‎benchmarks/README.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,12 @@ scenario there, `full_all_tuned` included, is reachable through `FastAPIConfig`.
600600
[sentry-python#7400](https://github.com/getsentry/sentry-python/issues/7400) and imports nothing
601601
from this directory.
602602

603+
Every number here was measured on sentry-sdk 2.67.1. The scripts also run on 3.x, which builds no
604+
transaction while tracing is off, so the patched scenarios, `errors_only_txn_endpoint_style` and
605+
`repro_sentry_txn.py --patched` exist only on 2.x. On 3.x, `micro.py` times `continue_trace` and
606+
the root `start_span` in place of the `Transaction` calls. 3.x also leaves `LoggingIntegration` out
607+
of its default integrations, so its `errors_only` records no log breadcrumbs.
608+
603609
Numbers are machine-specific and move a few percent run to run; the ratios and the ordering are
604610
the durable part. Everything here was measured in one session on one idle machine, which is the
605611
only way the columns are comparable to each other.

‎benchmarks/micro.py‎

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
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: `Transaction(...)` and the Mersenne Twister seed inside it are the
5-
two largest items, and neither is reachable from a configuration knob.
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.
68
"""
79

10+
import importlib
811
import statistics
912
import timeit
1013
import typing
@@ -14,9 +17,10 @@
1417
import sentry_scenarios
1518
import sentry_sdk
1619
from sentry_sdk.scope import Scope
17-
from sentry_sdk.tracing import Transaction
1820
from sentry_sdk.tracing_utils import Baggage, PropagationContext, _generate_sample_rand
1921

22+
from lite_bootstrap.instruments.sentry_instrument import IS_SENTRY_SDK_2
23+
2024

2125
HEADERS: typing.Final = {
2226
"host": "testserver",
@@ -25,6 +29,7 @@
2529
"connection": "keep-alive",
2630
}
2731
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
2833

2934

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

3641

37-
def main() -> None:
38-
sentry_sdk.init(**sentry_scenarios.base_kwargs(max_breadcrumbs=15, attach_stacktrace=True))
39-
scope = Scope.get_isolation_scope()
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")
4045

4146
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": {}}):
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):
4461
pass
4562

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+
67+
def main() -> None:
68+
sentry_sdk.init(**sentry_scenarios.base_kwargs(max_breadcrumbs=15, attach_stacktrace=True))
69+
scope = Scope.get_isolation_scope()
70+
4671
def isolation() -> None:
4772
with sentry_sdk.isolation_scope():
4873
pass
@@ -54,12 +79,10 @@ def isolation() -> None:
5479
timed("PropagationContext.from_incoming_data(headers)", lambda: PropagationContext.from_incoming_data(HEADERS))
5580
timed("Baggage.from_incoming_header(None)", lambda: Baggage.from_incoming_header(None))
5681
timed("scope.generate_propagation_context(headers)", lambda: scope.generate_propagation_context(HEADERS))
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)
82+
if IS_SENTRY_SDK_2:
83+
time_sdk2_transaction(scope, sentry_sdk.get_current_scope())
84+
else:
85+
time_sdk3_span()
6386
timed("isolation_scope() enter/exit", isolation)
6487
timed("scope.fork()", scope.fork)
6588
timed("get_client()", sentry_sdk.get_client)

‎benchmarks/repro_sentry_txn.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
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.
1012
"""
1113

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

6062

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

7779
def main() -> None:
7880
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")
7983
mode = "patched"
8084
elif "--off" in sys.argv:
8185
mode = "off"

‎benchmarks/sentry_scenarios.py‎

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@
44
`starlette.middleware.Middleware.__init__` and `logging.Logger.callHandlers`, and none of it
55
can be undone.
66
7-
The `*_patch` scenarios are not configurations a user can reach - they simulate an upstream
8-
fix so its value can be measured before proposing it.
7+
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`.
911
"""
1012

13+
import importlib
1114
import logging
1215
import typing
1316

1417
import sentry_sdk
15-
import sentry_sdk.tracing
1618
from driver import DSN, TRANSPORT
1719
from fastapi import FastAPI
1820
from sentry_sdk.integrations.dedupe import DedupeIntegration
@@ -25,6 +27,10 @@
2527
from sentry_sdk.integrations.threading import ThreadingIntegration
2628
from sentry_sdk.scope import Scope
2729

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
2834

2935
logger = logging.getLogger("bench")
3036

@@ -59,6 +65,17 @@ def _no_transaction_integrations() -> list[typing.Any]:
5965
]
6066

6167

68+
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)
77+
78+
6279
SCENARIOS: dict[str, typing.Callable[[], dict[str, typing.Any]] | None] = {
6380
"off": None,
6481
# lite-bootstrap's own defaults: tracing off, all default and auto integrations on.
@@ -113,24 +130,16 @@ def _no_transaction_integrations() -> list[typing.Any]:
113130
auto_enabling_integrations=False,
114131
integrations=[StarletteIntegration()],
115132
),
116-
"errors_only_txn_endpoint_style": lambda: base_kwargs(
117-
max_breadcrumbs=15,
118-
attach_stacktrace=True,
119-
integrations=[
120-
StarletteIntegration(transaction_style="endpoint"),
121-
FastApiIntegration(transaction_style="endpoint"),
122-
],
123-
),
124133
# --- logging integration knobs ---
125134
"errors_only_no_sentry_logs": lambda: base_kwargs(
126135
max_breadcrumbs=15,
127136
attach_stacktrace=True,
128-
integrations=[LoggingIntegration(sentry_logs_level=None)],
137+
integrations=[_errors_and_breadcrumbs_only(breadcrumbs=True)],
129138
),
130139
"errors_only_logging_lean": lambda: base_kwargs(
131140
max_breadcrumbs=15,
132141
attach_stacktrace=True,
133-
integrations=[LoggingIntegration(level=None, sentry_logs_level=None)],
142+
integrations=[_errors_and_breadcrumbs_only(breadcrumbs=False)],
134143
),
135144
# --- tracing on ---
136145
"traces_0": lambda: base_kwargs(traces_sample_rate=0.0, attach_stacktrace=True),
@@ -146,7 +155,7 @@ def _no_transaction_integrations() -> list[typing.Any]:
146155

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

151160

152161
def _patch_skip_transaction() -> None:
@@ -157,20 +166,30 @@ def continue_trace(
157166
) -> None:
158167
self.generate_propagation_context(environ_or_headers)
159168

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

162171

163-
PATCHES: dict[str, list[typing.Callable[[], None]]] = {
164-
"errors_only_lazy_sample_rand": [_patch_lazy_sample_rand],
165-
"errors_only_skip_txn": [_patch_skip_transaction],
166-
"errors_only_skip_txn_no_sessions": [_patch_skip_transaction],
167-
"errors_only_logging_lean_skip_txn": [_patch_skip_transaction],
168-
}
172+
PATCHES: dict[str, list[typing.Callable[[], None]]] = {}
169173

170-
SCENARIOS["errors_only_lazy_sample_rand"] = SCENARIOS["errors_only"]
171-
SCENARIOS["errors_only_skip_txn"] = SCENARIOS["errors_only"]
172-
SCENARIOS["errors_only_skip_txn_no_sessions"] = SCENARIOS["errors_only_no_sessions"]
173-
SCENARIOS["errors_only_logging_lean_skip_txn"] = SCENARIOS["errors_only_logging_lean"]
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"]
174193

175194

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

‎docs/introduction/configuration.md‎

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Additional parameters can also be supplied through the settings object:
1717
- `sentry_attach_stacktrace` - if True, stack traces are automatically attached to all messages logged
1818
- `sentry_auto_session_tracking` - whether every request opens and closes a Sentry release-health `Session` (default: `True`), measured at ~7.7 µs per request. Set it to `False` if you do not use Sentry release health.
1919
- `sentry_integrations` - list of integrations to enable
20-
- `sentry_logging_breadcrumb_level` - the minimum standard-library log level recorded as a breadcrumb (default: `logging.INFO`). Passed as `LoggingIntegration(level=...)`; see below.
20+
- `sentry_logging_breadcrumb_level` - the minimum standard-library log level recorded as a breadcrumb (default: `logging.INFO`). See below for how it reaches `LoggingIntegration`.
2121
- `sentry_tags` - key/value string pairs that are both indexed and searchable
2222
- `sentry_additional_params` - additional params, which will be passed to `sentry_sdk.init`, overriding any of the above that map to the same keyword
2323
- `sentry_default_integrations` - whether to use sentry's default integrations (default: `True`)
@@ -30,15 +30,21 @@ integration; see [Performance](performance.md).
3030

3131
### Sentry logging integration
3232

33-
Unless `sentry_integrations` already contains a `LoggingIntegration`, lite-bootstrap appends one built
34-
as `LoggingIntegration(level=sentry_logging_breadcrumb_level, sentry_logs_level=None)`. Its event
35-
handler keeps the sentry-sdk default (`ERROR`), so the only departure from sentry-sdk's own default
36-
integration is `sentry_logs_level`.
37-
38-
Turning `sentry_logs_level` off is free. lite-bootstrap never sets `enable_logs`, so Sentry Logs is
39-
off, but `SentryLogsHandler.emit` formats the record *before* it checks whether logs are enabled
40-
([getsentry/sentry-python#7402](https://github.com/getsentry/sentry-python/issues/7402)) - it formats
41-
every `INFO`+ record and discards the result. Dropping breadcrumbs as well, with
33+
lite-bootstrap supports sentry-sdk 2.70 and later, on both the 2.x and 3.x majors. Unless
34+
`sentry_integrations` already contains a `LoggingIntegration`, it appends one that turns `ERROR`
35+
records into events, records breadcrumbs from `sentry_logging_breadcrumb_level` up and sends no
36+
Sentry Logs. 3.0 renamed the integration's arguments, so the call differs per major:
37+
38+
- 2.x: `LoggingIntegration(level=sentry_logging_breadcrumb_level, sentry_logs_level=None)`, with the
39+
event level left at its `ERROR` default.
40+
- 3.x: `LoggingIntegration(breadcrumb_level=sentry_logging_breadcrumb_level, event_level=logging.ERROR, level=None)`.
41+
On 3.x `level` means Sentry Logs, and `event_level` defaults to `None`, so without it a logged
42+
error creates no event.
43+
44+
On 2.x, turning `sentry_logs_level` off is free. lite-bootstrap never sets `enable_logs`, so Sentry
45+
Logs is off, but `SentryLogsHandler.emit` formats the record *before* it checks whether logs are
46+
enabled ([getsentry/sentry-python#7402](https://github.com/getsentry/sentry-python/issues/7402)) - it
47+
formats every `INFO`+ record and discards the result. Dropping breadcrumbs as well, with
4248
`sentry_logging_breadcrumb_level=None`, saves more but costs you log breadcrumbs on error events, so
4349
it stays on by default. Both are measured on the [performance page](performance.md#where-the-time-goes).
4450

@@ -47,6 +53,27 @@ Two ways to opt out of the appended integration: supply your own `LoggingIntegra
4753
`sentry_default_integrations=False`, which suppresses it along with every other default integration.
4854
Under either, `sentry_logging_breadcrumb_level` is ignored and lite-bootstrap warns.
4955

56+
### Sentry data collection
57+
58+
lite-bootstrap passes an explicit `data_collection` to `sentry_sdk.init`, close to what
59+
`send_default_pii=False` collected: no user identity, HTTP bodies, GraphQL documents, generative AI or
60+
MCP inputs and outputs, database query data or queue message bodies. Request headers, cookies and URL
61+
query parameters are sent with IP- and user-identifying keys filtered. Stack frame variables are sent
62+
with secret-looking names (`password`, `token`, `key`, `session` and the rest of sentry-sdk's
63+
denylist) replaced by `[Filtered]`. The dict is `sentry_instrument.DEFAULT_DATA_COLLECTION`.
64+
65+
To collect more, pass your own `data_collection` in `sentry_additional_params`. It replaces the
66+
default as a whole, so keys you leave out take sentry-sdk's own defaults, which collect more. To
67+
change one key, spread the default: `{**DEFAULT_DATA_COLLECTION, "user_info": True}`. Read more about
68+
the option [here](https://docs.sentry.io/platforms/python/data-management/data-collected/).
69+
70+
`send_default_pii` has no effect on 2.x while `data_collection` is set, and on 3.x it is gone. 3.0
71+
also raises `TypeError` at `sentry_sdk.init` for any option it removed, so passing one of these
72+
through `sentry_additional_params` fails at bootstrap on 3.x, `enable_tracing`, `enable_logs` and
73+
`include_local_variables` among them. sentry-sdk's
74+
[migration guide](https://github.com/getsentry/sentry-python/blob/master/MIGRATION_GUIDE.md) lists
75+
every removed option.
76+
5077

5178
## Prometheus
5279

0 commit comments

Comments
 (0)