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 #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:
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 #22220, found while implementing the same consent-mode gating (@sentry/react@10.53.1). Happy to open a PR if this shape looks reasonable.
Problem Statement
browserSessionIntegration'ssetupOnce()permanently wires two triggers once installed, with no way to later disable them (there'sclient.addIntegration()but no symmetricremoveIntegration/teardown anywhere in the integration system):captureSession()whenever the scope's user id/ip changeslifecycle: 'route') a history-instrumentation handler that callsstartSession()/captureSession()on every navigationBoth bypass
beforeSend/beforeSendTransaction— same root cause as #22220, just for session envelopes instead of Replay. For consent-gated setups (CookieYes/OneTrust/Usercentrics/custom CMPs) that keepSentry.init()alive across consent grant/withdrawal and mute collection viabeforeSend,BrowserSessioncan'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 freshcaptureSession().Today the only mitigation is excluding
BrowserSessionfromintegrationsatSentry.init()entirely and permanently, losing Release Health even for users who did consent.Solution Brainstorm
Accept an
enabledpredicate, checked at all three capture sites:Default
enabled: () => true— fully backward compatible. Lets consent-driven setups passenabled: () => 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 #22220, found while implementing the same consent-mode gating (
@sentry/react@10.53.1). Happy to open a PR if this shape looks reasonable.