Skip to content

Commit 05346fb

Browse files
authored
Leave sentry-sdk's auto-enabling integrations off while Sentry tracing is off (#363)
Adds sentry_auto_enabling_integrations, passed as auto_enabling_integrations. None follows tracing on SentryConfig, and follows lean error capture on the Litestar, FastAPI and FastMCP configs, whose framework integrations are auto-enabling themselves. Closes #313.
1 parent 4e4e02e commit 05346fb

11 files changed

Lines changed: 242 additions & 12 deletions

File tree

‎benchmarks/README.md‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,10 +495,24 @@ lite-bootstrap logging on. The message triggers no work, so read the µs column:
495495
| Prometheus | +11.6 | |
496496
| OpenTelemetry, 100% | +73.0 | producer span plus context injection |
497497
| OpenTelemetry, 10% | +30.6 | |
498-
| Sentry | +28.8 | RedisIntegration (auto-enabling) on the redis client |
498+
| Sentry, 1.x defaults | +28.8 | RedisIntegration (auto-enabling) on the redis client |
499499
| Sentry, `auto_enabling_integrations=False` | +0.0 | |
500500
<!-- --8<-- [end:faststreampublish] -->
501501

502+
Since 2.0, lite-bootstrap leaves sentry-sdk's auto-enabling integrations off while Sentry tracing is
503+
off. Rerun per publish on the M2 host (macOS, CPython 3.14.7, faststream 0.7.8, redis 8.1.0, Redis 8 in a local
504+
container, `bench.py faststream log@pub,sentry@pub,sentry_auto@pub,sentry_noauto@pub --repeat 3`),
505+
over a baseline of about 136 µs:
506+
507+
| scenario | sentry-sdk 2.73.0 | 3.0.0b2 |
508+
|---|---:|---:|
509+
| `sentry`: the 2.0 default | -3.0 | -1.7 |
510+
| `sentry_auto`: `sentry_auto_enabling_integrations=True` | +27.1 | +9.5 |
511+
| `sentry_noauto`: `auto_enabling_integrations=False` in `sentry_additional_params` | -1.6 | -1.4 |
512+
513+
The first round of the baseline ran 11% slower than the other two, so the negative rows are noise.
514+
`sentry_noauto` now has the same config as `sentry`.
515+
502516
### 8e. FastMCP
503517

504518
<!-- --8<-- [start:fastmcp] -->

‎benchmarks/frameworks/faststream_suite.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"sentry_log_info": Scenario(config=base_config(**LOG, **sentry(), **INFO)),
5151
"sentry_log_handler3": Scenario(config=base_config(**LOG, **sentry()), kind="structlog"),
5252
"sentry_noauto": Scenario(config=base_config(**LOG, **sentry(auto_enabling_integrations=False))),
53+
"sentry_auto": Scenario(config=base_config(**LOG, **sentry(), sentry_auto_enabling_integrations=True)),
5354
"full": Scenario(config=base_config(**OTEL_MW, **PROM_MW, **LOG, **sentry())),
5455
}
5556
SCENARIOS: typing.Final = {**_BASE, **{f"{name}@pub": scenario for name, scenario in _BASE.items()}}

‎docs/integrations/fastapi.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@ Measured in-process on a do-nothing handler, over the same app without Sentry:
102102
| the same, request carries a sampled `sentry-trace` | +59.2 µs | +113.2 µs |
103103

104104
sentry-sdk records installed integrations for the whole process, and `disabled_integrations`
105-
cannot remove one that an earlier `sentry_sdk.init` installed. If that happened, lite-bootstrap
106-
warns that errors may be reported twice.
105+
cannot remove one that an earlier `sentry_sdk.init` installed. That matters only with
106+
[`sentry_auto_enabling_integrations`](../introduction/configuration.md#sentry-auto-enabling-integrations)
107+
on, since lean capture otherwise leaves auto-enabling off and sentry-sdk never enables the framework
108+
integration. With it on, lite-bootstrap warns that errors may be reported twice.
107109

108110
## Logging
109111

‎docs/integrations/fastmcp.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,14 @@ While Sentry tracing is off, lite-bootstrap reports errors in every application
7070
`application.http_app()` builds itself, instead of through sentry-sdk's `StarletteIntegration`.
7171
FastMCP's HTTP application is a Starlette application, so it works as it does
7272
[on FastAPI](fastapi.md#sentry): the same `sentry_lean_error_capture` rule, the same middleware and
73-
the same request data. `MCPIntegration` is left alone.
73+
the same request data. Lean capture also leaves
74+
[auto-enabling integrations](../introduction/configuration.md#sentry-auto-enabling-integrations) off,
75+
which drops `MCPIntegration` too. With tracing off it measured no cost per tool call, and the only
76+
difference it made to a tool error's event was a `parent_span_id` from its unsampled span.
7477

7578
A tool that raises does not fail the request: FastMCP answers with an error result and logs the
7679
exception, and the logging integration turns that record into one event with the `/mcp` request
77-
attached, with lean capture on or off.
80+
attached, with lean capture on or off and with `MCPIntegration` on or off.
7881

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

‎docs/integrations/litestar.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,10 @@ Measured in-process on a do-nothing handler, over the same app without Sentry:
150150
| the same, request carries a sampled `sentry-trace` | +74.4 µs | +147.4 µs |
151151

152152
sentry-sdk records installed integrations for the whole process, and `disabled_integrations`
153-
cannot remove one that an earlier `sentry_sdk.init` installed. If that happened, lite-bootstrap
154-
warns that errors may be reported twice.
153+
cannot remove one that an earlier `sentry_sdk.init` installed. That matters only with
154+
[`sentry_auto_enabling_integrations`](../introduction/configuration.md#sentry-auto-enabling-integrations)
155+
on, since lean capture otherwise leaves auto-enabling off and sentry-sdk never enables the framework
156+
integration. With it on, lite-bootstrap warns that errors may be reported twice.
155157

156158
## Prometheus
157159

‎docs/introduction/configuration.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Additional parameters can also be supplied through the settings object:
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`)
24+
- `sentry_auto_enabling_integrations` - whether sentry-sdk enables an integration for every installed package it knows, such as SQLAlchemy, asyncpg, redis and httpx (default: `None`, meaning off while Sentry tracing is off). See [Sentry auto-enabling integrations](#sentry-auto-enabling-integrations).
2425
- `sentry_before_send` - optional callback chained after the built-in structlog enricher, passed to `sentry_sdk.init(before_send=...)`
2526
- `sentry_lean_error_capture` - Litestar, FastAPI and FastMCP: report request errors without sentry-sdk's framework integration (default: `None`, meaning on while Sentry tracing is off). See [Litestar](../integrations/litestar.md#sentry), [FastAPI](../integrations/fastapi.md#sentry) and [FastMCP](../integrations/fastmcp.md#sentry).
2627

@@ -58,6 +59,31 @@ Two ways to opt out of the appended integration: supply your own `LoggingIntegra
5859
`sentry_default_integrations=False`, which suppresses it along with every other default integration.
5960
Under either, `sentry_logging_breadcrumb_level` is ignored and lite-bootstrap warns.
6061

62+
### Sentry auto-enabling integrations
63+
64+
sentry-sdk enables an integration for every installed package it has one for: the web frameworks,
65+
SQLAlchemy, asyncpg, redis, httpx, aiohttp, MCP and others. lite-bootstrap passes
66+
`auto_enabling_integrations` to `sentry_sdk.init`, and `sentry_auto_enabling_integrations` picks it:
67+
68+
- `None`, the default: off while Sentry tracing is off, on while it is on. On Litestar, FastAPI and
69+
FastMCP it follows [`sentry_lean_error_capture`](#sentry) instead: off with lean capture, on when
70+
sentry-sdk's framework integration reports request errors, because that integration is
71+
auto-enabling too.
72+
- `True` or `False`: that value always. An `auto_enabling_integrations` in `sentry_additional_params`
73+
still wins over both.
74+
75+
With tracing off, those integrations still open a span and record a breadcrumb for every SQL
76+
statement, Redis command or outgoing HTTP request. `SqlalchemyIntegration` measured about 64 µs per
77+
statement on sentry-sdk 2.70, and `RedisIntegration` +27.1 µs per FastStream publish on 2.73.0 and
78+
+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:
79+
dedupe, excepthook, threading, stdlib, atexit, modules, argv and lite-bootstrap's own
80+
`LoggingIntegration`.
81+
82+
Off, error events lose what those integrations add: SQL, Redis and outgoing-HTTP breadcrumbs, and
83+
anything else an auto-enabled integration attaches. To keep them, set
84+
`sentry_auto_enabling_integrations=True`, or list just the integrations you want in
85+
`sentry_integrations`, for example `[SqlalchemyIntegration()]`.
86+
6187
### Sentry data collection
6288

6389
lite-bootstrap passes an explicit `data_collection` to `sentry_sdk.init`, close to what

‎docs/introduction/performance.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,18 @@ request, every request then pays the per-record cost measured here:
6868

6969
--8<-- "benchmarks/README.md:logging"
7070

71-
lite-bootstrap already applies four savings for you. It turns Sentry Logs off in the logging
71+
lite-bootstrap already applies five savings for you. It turns Sentry Logs off in the logging
7272
integration, because it never enables them and on 2.x the handler formats every record before
7373
checking whether they are enabled; that one costs nothing. Since 2.0 it also leaves release-health
7474
sessions off and starts breadcrumbs at `WARNING`, so an `INFO` record skips the breadcrumb handler.
7575
Those two cost you release health and `INFO` breadcrumbs on error events; set
7676
`sentry_auto_session_tracking=True` or `sentry_logging_breadcrumb_level=logging.INFO` to get them
7777
back. And while Sentry tracing is off it reports request errors without sentry-sdk's framework
78-
integration, which costs you request transactions and bodies the handler never read; set `sentry_lean_error_capture=False` to keep the integration. The numbers on this page were
79-
measured before 2.0, against the old defaults.
78+
integration, which costs you request transactions and bodies the handler never read; set `sentry_lean_error_capture=False` to keep the integration. With tracing off it also turns
79+
sentry-sdk's [auto-enabling integrations](configuration.md#sentry-auto-enabling-integrations) off,
80+
which costs you their SQL, Redis and outgoing-HTTP breadcrumbs; set
81+
`sentry_auto_enabling_integrations=True` to keep them. The numbers on this page were measured
82+
before 2.0, against the old defaults.
8083

8184
## What each saving costs you
8285

‎lite_bootstrap/instruments/sentry_instrument.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,14 @@ class SentryConfig(BaseConfig):
5555
sentry_additional_params: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
5656
sentry_tags: dict[str, str] | None = None
5757
sentry_default_integrations: bool = True
58+
sentry_auto_enabling_integrations: bool | None = None
5859
sentry_before_send: "sentry_types.EventProcessor | None" = None
5960

61+
def is_sentry_auto_enabling_integrations_on(self) -> bool:
62+
if self.sentry_auto_enabling_integrations is not None:
63+
return self.sentry_auto_enabling_integrations
64+
return self.is_sentry_tracing_requested()
65+
6066
def is_sentry_tracing_requested(self) -> bool:
6167
return any(
6268
option is not None
@@ -101,6 +107,11 @@ def is_sentry_lean_error_capture_on(self) -> bool:
101107
return self.sentry_lean_error_capture
102108
return not self.is_sentry_tracing_requested() and not self._supplied_sentry_lean_replaced_integrations()
103109

110+
def is_sentry_auto_enabling_integrations_on(self) -> bool:
111+
if self.sentry_auto_enabling_integrations is not None:
112+
return self.sentry_auto_enabling_integrations
113+
return not self.is_sentry_lean_error_capture_on()
114+
104115

105116
SENTRY_FAILED_REQUEST_STATUS_CODES: typing.Final = range(500, 600)
106117
_SENTRY_TRACE_HEADERS: typing.Final = frozenset({b"sentry-trace", b"baggage"})
@@ -233,6 +244,7 @@ def _build_init_params(self) -> dict[str, typing.Any]:
233244
"auto_session_tracking": config.sentry_auto_session_tracking,
234245
"integrations": self._build_integrations(),
235246
"default_integrations": config.sentry_default_integrations,
247+
"auto_enabling_integrations": config.is_sentry_auto_enabling_integrations_on(),
236248
"data_collection": DEFAULT_DATA_COLLECTION,
237249
"before_send": wrap_before_send_callbacks(
238250
enrich_sentry_event_from_structlog_log, config.sentry_before_send

‎tests/instruments/test_sentry_instrument.py‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,85 @@ def test_sentry_does_not_warn_when_the_breadcrumb_level_is_left_at_its_default(
381381
assert [one for one in recwarn if "sentry_logging_breadcrumb_level" in str(one.message)] == []
382382
finally:
383383
instrument.teardown()
384+
385+
386+
_AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT: typing.Final = frozenset({"aiohttp", "httpx", "redis"})
387+
388+
389+
def _active_integrations(bootstrap_config: SentryConfig) -> set[str]:
390+
instrument = SentryInstrument(bootstrap_config=bootstrap_config)
391+
instrument.bootstrap()
392+
try:
393+
return set(sentry_sdk.get_client().integrations)
394+
finally:
395+
instrument.teardown()
396+
397+
398+
def test_sentry_leaves_auto_enabling_integrations_off_while_tracing_is_off(
399+
minimal_sentry_config: SentryConfig,
400+
) -> None:
401+
active = _active_integrations(minimal_sentry_config)
402+
403+
assert not _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT & active
404+
assert "logging" in active
405+
406+
407+
@pytest.mark.parametrize(
408+
"tracing_overrides",
409+
[
410+
pytest.param({"sentry_traces_sample_rate": 0.0}, id="sentry_traces_sample_rate"),
411+
pytest.param({"sentry_additional_params": {"traces_sample_rate": 0.5}}, id="traces_sample_rate"),
412+
pytest.param({"sentry_additional_params": {"traces_sampler": lambda _: 0.5}}, id="traces_sampler"),
413+
],
414+
)
415+
def test_sentry_keeps_auto_enabling_integrations_while_tracing_is_on(
416+
minimal_sentry_config: SentryConfig, tracing_overrides: dict[str, typing.Any]
417+
) -> None:
418+
additional_params = {
419+
**minimal_sentry_config.sentry_additional_params,
420+
**tracing_overrides.pop("sentry_additional_params", {}),
421+
}
422+
bootstrap_config = dataclasses.replace(
423+
minimal_sentry_config, sentry_additional_params=additional_params, **tracing_overrides
424+
)
425+
426+
assert _active_integrations(bootstrap_config) >= _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT
427+
428+
429+
@pytest.mark.parametrize(
430+
("auto_enabling_integrations", "traces_sample_rate", "expected"),
431+
[
432+
pytest.param(True, None, True, id="on-while-tracing-is-off"),
433+
pytest.param(False, 1.0, False, id="off-while-tracing-is-on"),
434+
],
435+
)
436+
def test_sentry_auto_enabling_integrations_set_explicitly_wins(
437+
minimal_sentry_config: SentryConfig,
438+
auto_enabling_integrations: bool,
439+
traces_sample_rate: float | None,
440+
expected: bool,
441+
) -> None:
442+
bootstrap_config = dataclasses.replace(
443+
minimal_sentry_config,
444+
sentry_auto_enabling_integrations=auto_enabling_integrations,
445+
sentry_traces_sample_rate=traces_sample_rate,
446+
)
447+
instrument = SentryInstrument(bootstrap_config=bootstrap_config)
448+
instrument.bootstrap()
449+
450+
try:
451+
assert sentry_sdk.get_client().options["auto_enabling_integrations"] is expected
452+
finally:
453+
instrument.teardown()
454+
455+
456+
def test_sentry_additional_params_auto_enabling_integrations_wins_over_the_field(
457+
minimal_sentry_config: SentryConfig,
458+
) -> None:
459+
bootstrap_config = dataclasses.replace(
460+
minimal_sentry_config,
461+
sentry_auto_enabling_integrations=False,
462+
sentry_additional_params={**minimal_sentry_config.sentry_additional_params, "auto_enabling_integrations": True},
463+
)
464+
465+
assert _active_integrations(bootstrap_config) >= _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT

‎tests/test_litestar_sentry_capture.py‎

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,26 @@ def test_lean_capture_reports_once_when_litestar_logs_the_exception(
419419
assert _exception_types(sentry_mock) == ["RuntimeError"]
420420

421421

422+
_AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT: typing.Final = frozenset({"aiohttp", "httpx", "redis"})
423+
424+
425+
def test_lean_capture_leaves_auto_enabling_integrations_off_and_still_reports(sentry_mock: SentryTestTransport) -> None:
426+
application = LitestarBootstrapper(bootstrap_config=_build_config(sentry_mock)).bootstrap()
427+
with TestClient(app=application, raise_server_exceptions=False) as test_client:
428+
active = test_client.get("/integrations").json()
429+
test_client.get("/boom")
430+
431+
assert not _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT & set(active)
432+
[event] = _error_events(sentry_mock)
433+
assert event["request"]["url"] == "http://testserver.local/boom"
434+
435+
436+
def test_integration_capture_keeps_auto_enabling_integrations(sentry_mock: SentryTestTransport) -> None:
437+
active = _active_integrations(sentry_mock, sentry_lean_error_capture=False)
438+
439+
assert set(active) >= _AUTO_ENABLED_IN_THE_TEST_ENVIRONMENT
440+
441+
422442
def test_lean_capture_replaces_the_litestar_integration(sentry_mock: SentryTestTransport) -> None:
423443
active = _active_integrations(sentry_mock, sentry_lean_error_capture=True)
424444

@@ -441,13 +461,27 @@ def test_lean_capture_warns_when_an_earlier_init_installed_the_litestar_integrat
441461
sentry_mock: SentryTestTransport,
442462
) -> None:
443463
sentry_integrations._installed_integrations.add(LitestarIntegration.identifier) # noqa: SLF001
444-
bootstrapper = LitestarBootstrapper(bootstrap_config=_build_config(sentry_mock))
464+
bootstrapper = LitestarBootstrapper(
465+
bootstrap_config=_build_config(sentry_mock, sentry_auto_enabling_integrations=True)
466+
)
445467

446468
with pytest.warns(UserWarning, match="installed by an earlier sentry_sdk.init"):
447469
bootstrapper.bootstrap()
448470
bootstrapper.teardown()
449471

450472

473+
def test_lean_capture_ignores_an_earlier_installed_litestar_integration_while_auto_enabling_is_off(
474+
sentry_mock: SentryTestTransport,
475+
) -> None:
476+
sentry_integrations._installed_integrations.add(LitestarIntegration.identifier) # noqa: SLF001
477+
478+
with warnings.catch_warnings():
479+
warnings.simplefilter("error")
480+
active = _active_integrations(sentry_mock)
481+
482+
assert LitestarIntegration.identifier not in active
483+
484+
451485
def test_forced_lean_capture_warns_that_traces_sample_rate_gets_no_request_transactions(
452486
sentry_mock: SentryTestTransport,
453487
) -> None:

0 commit comments

Comments
 (0)