Skip to content

browserSessionIntegration has no way to suppress session capture for consent-gated setups (GDPR) #22261

Description

@trinitiwowka

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.

Metadata

Metadata

Assignees

No one assigned

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Waiting for: Product Owner

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions