getsentry / getsentry/sentry-javascript
browserSessionIntegration has no way to suppress session capture for consent-gated setups (GDPR)
- Langage dominant
- TypeScript
- Étoiles
- 8.7k
- Forks
- 1.8k
- Merge moyen
- 1 j 17 h
- PR mergées (30 j)
- 515
Description
## Problem Statement
`browserSessionIntegration`'s `setupOnce()` permanently wires two triggers once installed, with no way to later disable them (there's `client.addIntegration()` but no symmetric `removeIntegration`/teardown anywhere in the integration system):
* an isolation-scope listener that calls `captureSession()` whenever the scope's user id/ip changes
* (default `lifecycle: 'route'`) a history-instrumentation handler that calls `startSession()`/`captureSession()` on every navigation
Both bypass `beforeSend`/`beforeSendTransaction` — same root cause as getsentry/sentry-javascript#22220, just for session envelopes instead of Replay. For consent-gated setups (CookieYes/OneTrust/Usercentrics/custom CMPs) that keep `Sentry.init()` alive across consent grant/withdrawal and mute collection via `beforeSend`, `BrowserSession` can't be muted the same way.
Worse, it's self-triggering: consent-withdrawal code commonly does `getIsolationScope().clear()` to guarantee nothing written pre-consent leaks into a later event — but that exact mutation is what the scope listener watches for, so withdrawing consent immediately fires a fresh `captureSession()`.
Today the only mitigation is excluding `BrowserSession` from `integrations` at `Sentry.init()` entirely and permanently, losing Release Health even for users who did consent.
## Solution Brainstorm
Accept an `enabled` predicate, checked at all three capture sites:
```ts
const browserSessionIntegration = defineIntegration((options: { lifecycle?: 'route' | 'manual'; enabled?: () => boolean } = {}) => {
const lifecycle = options.lifecycle ?? 'route';
const isEnabled = () => options.enabled?.() ?? true;
return {
name: 'BrowserSession',
setupOnce() {
// ...
if (isEnabled()) { startSession({ ignoreDuration: true }); captureSession(); }
isolationScope.addScopeListener(scope => {
const maybeNewUser = scope.getUser();
if (isEnabled() && (previousUser?.id !== maybeNewUser?.id || previousUser?.ip_address !== maybeNewUser?.ip_address)) {
captureSession();
previousUser = maybeNewUser;
}
});
if (lifecycle === 'route') {
addHistoryInstrumentationHandler(({ from, to }) => {
if (isEnabled() && from !== to) { startSession({ ignoreDuration: true }); captureSession(); }
});
}
},
};
});
```
Default `enabled: () => true` — fully backward compatible. Lets consent-driven setups pass `enabled: () => consentGranted`, keep the integration permanently installed (correct init ordering for other instrumentation), and suppress session capture whenever consent isn't currently granted — including the withdrawal-triggered scope-clear case above.
## Additional Context
Same category as getsentry/sentry-javascript#22220, found while implementing the same consent-mode gating (`@sentry/react@10.53.1`). Happy to open a PR if this shape looks reasonable.
Guide de contribution
Ouvrir le guide de contribution
Évaluation
Cette issue n'a pas encore été évaluée.