Skip to content

Session Replay (rrweb) blocks main thread when Zendesk Web Widget is present #1820

@sbimochan

Description

@sbimochan

Is there an existing issue for this?

Environment

  • SaaS (sentry.io)
  • SDK: @sentry/react (also reproducible with @sentry/browser)
  • SDK Version: [fill in your version, e.g. 8.x.x]
  • Browser: Chrome 130+ (reproducible across browsers)
  • Framework: React (Vite dev server, localhost:3000)

Problem Statement

When Session Replay is enabled (replaysSessionSampleRate > 0 or replaysOnErrorSampleRate > 0), interacting with the Zendesk Web Widget causes significant main thread blocking. The UI becomes unresponsive — clicks are delayed, scrolling stutters, and Sentry itself reports ui.long-task spans with the transaction name Main UI thread blocked.

Setting both replaysSessionSampleRate and replaysOnErrorSampleRate to 0 completely eliminates the thread blocking, confirming that rrweb's DOM observation/serialization is the root cause — not the Zendesk widget's own JavaScript.

Steps to Reproduce

  1. Initialize Sentry with Session Replay enabled:
Sentry.init({
  dsn: "YOUR_DSN",
  replaysSessionSampleRate: 1.0,
  replaysOnErrorSampleRate: 1.0,
  integrations: [
    Sentry.replayIntegration(),
  ],
});
  1. Load the Zendesk Web Widget on the same page (standard snippet from Zendesk):
<script id="ze-snippet" src="https://static.zdassets.com/ekr/snippet.js?key=YOUR_KEY"></script>
  1. Click on the Zendesk widget launcher button to open the widget.
  2. Observe main thread blocking in DevTools Performance tab and Sentry logs.

Observed Behavior

Console output from Sentry debug mode shows continuous span creation:

Sentry Logger [log]: [Tracing] Starting 'http.client' span on transaction 'GET https://ekr.zdassets.com/compose/...'
Sentry Logger [log]: [Tracing] Starting 'ui.long-task' span on transaction 'Main UI thread blocked'
Sentry Logger [log]: [Tracing] Starting 'http.client' span on transaction 'GET http://localhost:3000/ap/v2/users/me/zendesk-help-center-jwt'
Sentry Logger [log]: [Tracing] Starting 'ui.long-task' span on transaction 'Main UI thread blocked'

The ui.long-task spans occur repeatedly when the Zendesk widget is interacted with, indicating the main thread is blocked for >50ms per task.

Expected Behavior

The block selector configuration in replayIntegration should prevent rrweb from observing Zendesk widget DOM nodes and their mutations, eliminating the main thread overhead. The application should remain responsive when interacting with the Zendesk widget while Session Replay is active.

What I've Already Tried

1. Blocking Zendesk DOM elements via block selectors

Sentry.replayIntegration({
  block: [
    '#launcher',
    '#webWidget',
    'iframe[src*="zdassets.com"]',
    'iframe[src*="zendesk.com"]',
    'iframe[title*="Zendesk"]',
    '[data-product="web_widget"]',
    '.zEWidget-launcher',
  ],
})

Result: Did not resolve the thread blocking.

2. Adding data-sentry-block via MutationObserver before Sentry.init()

// Runs BEFORE Sentry.init()
const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (!(node instanceof HTMLElement)) continue;
      const isZendesk =
        node.id === 'launcher' ||
        node.id === 'webWidget' ||
        node.getAttribute?.('data-product') === 'web_widget' ||
        (node.tagName === 'IFRAME' &&
          (node.src?.includes('zdassets.com') || node.src?.includes('zendesk.com')));
      if (isZendesk) {
        node.setAttribute('data-sentry-block', 'true');
      }
    }
  }
});
observer.observe(document.body, { childList: true, subtree: true });

Result: Did not resolve the thread blocking.

3. Dropping Zendesk network spans via beforeAddRecordingEvent

Sentry.replayIntegration({
  beforeAddRecordingEvent: (event) => {
    if (event.data?.tag === 'performanceSpan') {
      const desc = event.data?.payload?.description || '';
      if (/zdassets|zendesk|zopim|smooch|zendesk-help-center-jwt/.test(desc)) {
        return null;
      }
    }
    return event;
  },
})

Result: Successfully drops Zendesk-related recording events from the replay payload, but does not reduce main thread blocking. The overhead comes from rrweb's MutationObserver processing, which happens before beforeAddRecordingEvent is called.

4. Filtering via denyUrls, beforeSendTransaction, beforeSendSpan

Sentry.init({
  denyUrls: [/\.zdassets\.com/, /\.zendesk\.com/, /\.zopim\.com/, /\.smooch\.io/],
  tracePropagationTargets: ['localhost', /^https:\/\/yourdomain\.com/],
  beforeSendTransaction(tx) {
    if (/zdassets|zendesk|zopim|smooch/.test(tx.transaction || '')) return null;
    return tx;
  },
  beforeSendSpan(span) {
    if (/zdassets|zendesk|zopim|smooch/.test(span.description || '')) return null;
    return span;
  },
});

Result: Reduces noise in Sentry dashboard but does not fix the main thread blocking since the overhead is in rrweb's internal DOM observation, not in event transmission.

5. Disabling long-task tracking

Sentry.browserTracingIntegration({
  enableLongTask: false,
  enableLongAnimationFrame: false,
})

Result: Stops reporting the symptom but does not fix the actual thread blocking.

6. Setting replay sample rates to 0

replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,

Result: : This completely eliminates the thread blocking, confirming rrweb is the cause.

Analysis

The Zendesk Web Widget injects multiple cross-origin iframes and triggers frequent DOM mutations (iframe creation, style changes, dynamic element injection). When rrweb's MutationObserver is active, it attempts to serialize these mutations on the main thread. The block selector configuration does not appear to fully prevent rrweb from observing mutations in or around the Zendesk widget's DOM subtree, particularly:

  • Cross-origin iframes that Zendesk injects dynamically after page load
  • The wrapper div elements and shadow DOM structures Zendesk creates
  • Style mutations and attribute changes on Zendesk elements

The core issue seems to be that rrweb's MutationObserver is attached to the document/body level and processes all mutations, including those from blocked elements, before the block filtering can take effect. The CPU cost is in the observation and serialization phase, not in the transmission phase — which is why beforeAddRecordingEvent, denyUrls, and other downstream filters don't help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions