Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 0 additions & 19 deletions .github/workflows/_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,6 @@ jobs:
- run: uv python pin ${{ matrix.python-version }}
- run: just install
- run: just test-ci
sentry-majors:
# The pytest jobs resolve whichever sentry-sdk major is newest; this runs the suite on each one.
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
sentry-sdk: ["sentry-sdk<3", "sentry-sdk==3.0.0b2"]
steps:
- uses: actions/checkout@v6
- uses: extractions/setup-just@v4
- uses: astral-sh/setup-uv@v8.2.0
with:
enable-cache: true
cache-dependency-glob: "**/pyproject.toml"
- run: uv python install 3.14
- run: uv python pin 3.14
- run: just install
- run: uv pip install "${{ matrix.sentry-sdk }}"
- run: just test
free-threaded:
runs-on: ubuntu-latest
strategy:
Expand Down
6 changes: 1 addition & 5 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -721,11 +721,7 @@ scenario there, `full_all_tuned` included, is reachable through `FastAPIConfig`.
[sentry-python#7400](https://github.com/getsentry/sentry-python/issues/7400) and imports nothing
from this directory.

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

Numbers are machine-specific and move a few percent run to run; the ratios and the ordering are
the durable part. Everything here was measured in one session on one idle machine, which is the
Expand Down
51 changes: 14 additions & 37 deletions benchmarks/micro.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
"""Per-operation costs of the pieces sentry-sdk runs on every request.

Attributes the per-request total from `run.py` to individual calls, which is what makes the
upstream issues actionable: on sentry-sdk 2.x, `Transaction(...)` and the Mersenne Twister seed
inside it are the two largest items, and neither is reachable from a configuration knob. 3.x builds
no transaction while tracing is off, so there it times `continue_trace` and the root `start_span`
that the ASGI integration calls on every request instead.
upstream issues actionable: `Transaction(...)` and the Mersenne Twister seed inside it are the
two largest items, and neither is reachable from a configuration knob.
"""

import importlib
import statistics
import timeit
import typing
Expand All @@ -17,10 +14,9 @@
import sentry_scenarios
import sentry_sdk
from sentry_sdk.scope import Scope
from sentry_sdk.tracing import Transaction
from sentry_sdk.tracing_utils import Baggage, PropagationContext, _generate_sample_rand

from lite_bootstrap.instruments.sentry_instrument import IS_SENTRY_SDK_2


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


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


def time_sdk2_transaction(isolation_scope: typing.Any, current_scope: typing.Any) -> None: # noqa: ANN401
def new_transaction() -> object:
return SDK2_TRACING.Transaction(op="http.server", name="GET /ping", source="route")

def start_and_finish() -> None:
with current_scope.start_transaction(new_transaction(), custom_sampling_context={"asgi_scope": {}}):
pass

timed(
"scope.continue_trace(headers)",
lambda: isolation_scope.continue_trace(HEADERS, op="http.server", name="GET /ping", source="route"),
)
timed("Transaction(op, name, source)", new_transaction)
timed("start_transaction(txn) + exit (tracing off)", start_and_finish, number=5000)


def time_sdk3_span() -> None:
def start_and_end() -> None:
with sentry_sdk.start_span(name="GET /ping", parent_span=None):
pass

timed("continue_trace(headers)", lambda: sentry_sdk.continue_trace(HEADERS))
timed("start_span(root) + exit (tracing off)", start_and_end, number=5000)


def main() -> None:
sentry_sdk.init(**sentry_scenarios.base_kwargs(max_breadcrumbs=15, attach_stacktrace=True))
scope = Scope.get_isolation_scope()

def start_and_finish() -> None:
transaction = Transaction(op="http.server", name="GET /ping", source="route")
with sentry_sdk.start_transaction(transaction, custom_sampling_context={"asgi_scope": {}}):
pass

def isolation() -> None:
with sentry_sdk.isolation_scope():
pass
Expand All @@ -79,10 +54,12 @@ def isolation() -> None:
timed("PropagationContext.from_incoming_data(headers)", lambda: PropagationContext.from_incoming_data(HEADERS))
timed("Baggage.from_incoming_header(None)", lambda: Baggage.from_incoming_header(None))
timed("scope.generate_propagation_context(headers)", lambda: scope.generate_propagation_context(HEADERS))
if IS_SENTRY_SDK_2:
time_sdk2_transaction(scope, sentry_sdk.get_current_scope())
else:
time_sdk3_span()
timed(
"scope.continue_trace(headers)",
lambda: scope.continue_trace(HEADERS, op="http.server", name="GET /ping", source="route"),
)
timed("Transaction(op, name, source)", lambda: Transaction(op="http.server", name="GET /ping", source="route"))
timed("start_transaction(txn) + exit (tracing off)", start_and_finish, number=5000)
timed("isolation_scope() enter/exit", isolation)
timed("scope.fork()", scope.fork)
timed("get_client()", sentry_sdk.get_client)
Expand Down
6 changes: 1 addition & 5 deletions benchmarks/repro_sentry_txn.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
python repro_sentry_txn.py --off # SDK never initialised
python repro_sentry_txn.py # SDK as shipped
python repro_sentry_txn.py --patched # skip the Transaction, keep trace propagation

`--patched` runs on sentry-sdk 2.x only: 3.x builds no transaction while tracing is off.
"""

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


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

def main() -> None:
if "--patched" in sys.argv:
if not sentry_sdk.VERSION.startswith("2."):
sys.exit(f"--patched needs sentry-sdk 2.x; {sentry_sdk.VERSION} builds no transaction to skip")
mode = "patched"
elif "--off" in sys.argv:
mode = "off"
Expand Down
63 changes: 25 additions & 38 deletions benchmarks/sentry_scenarios.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,14 @@
can be undone.

The patched scenarios are not configurations a user can reach - they simulate an upstream
fix so its value can be measured before proposing it. They, and `errors_only_txn_endpoint_style`,
exist only on sentry-sdk 2.x: 3.x builds no transaction while tracing is off and has no
`transaction_style`.
fix so its value can be measured before proposing it.
"""

import importlib
import logging
import typing

import sentry_sdk
import sentry_sdk.tracing
from driver import DSN, TRANSPORT
from fastapi import FastAPI
from sentry_sdk.integrations.dedupe import DedupeIntegration
Expand All @@ -27,10 +25,6 @@
from sentry_sdk.integrations.threading import ThreadingIntegration
from sentry_sdk.scope import Scope

from lite_bootstrap.instruments.sentry_instrument import IS_SENTRY_SDK_2


SDK2_TRACING: typing.Final = importlib.import_module("sentry_sdk.tracing") if IS_SENTRY_SDK_2 else None

logger = logging.getLogger("bench")

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


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


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

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


def _patch_skip_transaction() -> None:
Expand All @@ -166,30 +154,29 @@ def continue_trace(
) -> None:
self.generate_propagation_context(environ_or_headers)

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


PATCHES: dict[str, list[typing.Callable[[], None]]] = {}
SCENARIOS["errors_only_txn_endpoint_style"] = lambda: base_kwargs(
max_breadcrumbs=15,
attach_stacktrace=True,
integrations=[
StarletteIntegration(transaction_style="endpoint"),
FastApiIntegration(transaction_style="endpoint"),
],
)

if IS_SENTRY_SDK_2:
endpoint_style: dict[str, typing.Any] = {"transaction_style": "endpoint"}
SCENARIOS["errors_only_txn_endpoint_style"] = lambda: base_kwargs(
max_breadcrumbs=15,
attach_stacktrace=True,
integrations=[StarletteIntegration(**endpoint_style), FastApiIntegration(**endpoint_style)],
)
PATCHES.update(
{
"errors_only_lazy_sample_rand": [_patch_lazy_sample_rand],
"errors_only_skip_txn": [_patch_skip_transaction],
"errors_only_skip_txn_no_sessions": [_patch_skip_transaction],
"errors_only_logging_lean_skip_txn": [_patch_skip_transaction],
}
)
SCENARIOS["errors_only_lazy_sample_rand"] = SCENARIOS["errors_only"]
SCENARIOS["errors_only_skip_txn"] = SCENARIOS["errors_only"]
SCENARIOS["errors_only_skip_txn_no_sessions"] = SCENARIOS["errors_only_no_sessions"]
SCENARIOS["errors_only_logging_lean_skip_txn"] = SCENARIOS["errors_only_logging_lean"]
PATCHES: dict[str, list[typing.Callable[[], None]]] = {
"errors_only_lazy_sample_rand": [_patch_lazy_sample_rand],
"errors_only_skip_txn": [_patch_skip_transaction],
"errors_only_skip_txn_no_sessions": [_patch_skip_transaction],
"errors_only_logging_lean_skip_txn": [_patch_skip_transaction],
}

SCENARIOS["errors_only_lazy_sample_rand"] = SCENARIOS["errors_only"]
SCENARIOS["errors_only_skip_txn"] = SCENARIOS["errors_only"]
SCENARIOS["errors_only_skip_txn_no_sessions"] = SCENARIOS["errors_only_no_sessions"]
SCENARIOS["errors_only_logging_lean_skip_txn"] = SCENARIOS["errors_only_logging_lean"]


def setup(scenario: str) -> None:
Expand Down
12 changes: 6 additions & 6 deletions docs/integrations/fastapi.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ event either way.

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

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

sentry-sdk records installed integrations for the whole process, and `disabled_integrations`
cannot remove one that an earlier `sentry_sdk.init` installed. That matters only with
Expand Down
10 changes: 5 additions & 5 deletions docs/integrations/fastmcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ attached, with lean capture on or off and with `MCPIntegration` on or off.

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

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

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

Expand Down
13 changes: 6 additions & 7 deletions docs/integrations/litestar.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ sentry-sdk's `LitestarIntegration`. Tracing counts as off when `sentry_traces_sa
and `sentry_additional_params` sets none of `traces_sample_rate`, `traces_sampler` and
`enable_tracing`. With tracing off, the integration still builds a transaction and a span per
middleware for every request and then discards them
([getsentry/sentry-python#7400](https://github.com/getsentry/sentry-python/issues/7400)). On
sentry-sdk 3.0, an incoming `sentry-trace` header marked as sampled makes it record a real span.
([getsentry/sentry-python#7400](https://github.com/getsentry/sentry-python/issues/7400)).

`sentry_lean_error_capture` picks the path:

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

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

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

sentry-sdk records installed integrations for the whole process, and `disabled_integrations`
cannot remove one that an earlier `sentry_sdk.init` installed. That matters only with
Expand Down
31 changes: 11 additions & 20 deletions docs/introduction/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,13 @@ Litestar, FastAPI and FastMCP while Sentry tracing is off; see [Performance](per

### Sentry logging integration

lite-bootstrap supports sentry-sdk 2.70 and later, on both the 2.x and 3.x majors. Unless
`sentry_integrations` already contains a `LoggingIntegration`, it appends one that turns `ERROR`
records into events, records breadcrumbs from `sentry_logging_breadcrumb_level` up and sends no
Sentry Logs. 3.0 renamed the integration's arguments, so the call differs per major:

- 2.x: `LoggingIntegration(level=sentry_logging_breadcrumb_level, sentry_logs_level=None)`, with the
event level left at its `ERROR` default.
- 3.x: `LoggingIntegration(breadcrumb_level=sentry_logging_breadcrumb_level, event_level=logging.ERROR, level=None)`.
On 3.x `level` means Sentry Logs, and `event_level` defaults to `None`, so without it a logged
error creates no event.

On 2.x, turning `sentry_logs_level` off is free. lite-bootstrap never sets `enable_logs`, so Sentry
lite-bootstrap supports sentry-sdk 2.70 and later 2.x releases. sentry-sdk 3.0 is not supported
yet. Unless `sentry_integrations` already contains a `LoggingIntegration`, lite-bootstrap appends
`LoggingIntegration(level=sentry_logging_breadcrumb_level, sentry_logs_level=None)`. It turns
`ERROR` records into events, records breadcrumbs from `sentry_logging_breadcrumb_level` up and sends
no Sentry Logs.

Turning `sentry_logs_level` off is free. lite-bootstrap never sets `enable_logs`, so Sentry
Logs is off, but `SentryLogsHandler.emit` formats the record *before* it checks whether logs are
enabled ([getsentry/sentry-python#7402](https://github.com/getsentry/sentry-python/issues/7402)) - it
formats every `INFO`+ record and discards the result.
Expand Down Expand Up @@ -92,8 +87,8 @@ SQLAlchemy, asyncpg, redis, httpx, aiohttp, MCP and others. lite-bootstrap passe

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

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

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


## Prometheus
Expand Down
Loading
Loading