[Replay][Safari] Visual state freezes around the 60-second error-buffer checkout while non-DOM events continue

Open
#24,564 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
42/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
angular, typescript

Research direction

Start at the Replay integration's 60,000 ms error-buffer checkout and the rrweb full-snapshot serializer path described in the report; reproduce in Safari if possible and preserve the original exception while tracing the lock, snapshot, and unlock sequence. Done means a failed checkout cannot leave DOM mutation recording locked, opaque global rejections are contained, and later visual state remains consistent with the Replay timeline.

Written by the indexing model from the issue text.

Description

Angular Browser Bug Replays Waiting for: Product Owner
Is there an existing issue for this?
How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/angular

SDK Version

10.75.0

Framework Version

Angular: 22.1.7

Link to Sentry event

https://remotemdr-9a8bcf225.sentry.io/issues/5748192625/events/9e7fd03395c041b7b3f8721468a6b072/?project=4505866220732416

Reproduction Example/SDK Setup

We do not currently have a deterministic public reproduction. The issue has been observed across multiple production Replays in Safari on macOS.

The relevant SDK setup is equivalent to:

Sentry.init({
  integrations: [Sentry.browserTracingIntegration()],
  replaysSessionSampleRate: 0,
  replaysOnErrorSampleRate: 1.0,
});

Sentry.addIntegration(
  Sentry.replayIntegration({
    maskAllText: true,
    maskAllInputs: true,
    blockAllMedia: true,
    beforeErrorSampling: (event) => {
      if (!event.message) {
        return true;
      }

      return !ignoredMessages.some((message) =>
        event.message?.includes(message),
      );
    },
  }),
);

The Replay integration is added after the Angular application has bootstrapped. Additional block, unblock, and unmask selectors have been omitted because they only target ordinary application elements.

The application uses Angular 22.1.7 without zone.js.

Steps to Reproduce

This is not yet deterministically reproducible outside production. The following sequence describes the behavior observed in affected Replays:

  1. Configure Replay for error-triggered buffering with replaysSessionSampleRate: 0 and replaysOnErrorSampleRate: 1.0.
  2. Open the Angular SPA in desktop Safari on macOS.
  3. Navigate within the SPA and interact with a page containing normal dynamic DOM activity.
  4. Keep the page open beyond the first 60-second Replay checkout interval.
  5. Around that boundary, observe opaque unhandledrejection events in Sentry.
  6. Continue using the application and navigate to another SPA route.
  7. Open the resulting Replay.
  8. Compare the Replay’s current URL and activity timeline with its rendered viewport.

In a representative Replay:

  • The first opaque rejection occurred approximately 61.8 seconds after Replay began.
  • Another CustomEvent rejection occurred approximately 60.1 seconds later.
  • Subsequent navigation, network, console, WebSocket, media-session, and application diagnostic activity continued.
  • The rendered Replay viewport remained permanently stuck on the earlier page.

Most of the affected Replays we manually reviewed show the same frozen-visual-state pattern.

Expected Result

A failure during a periodic Replay checkout should be contained within Replay.

Specifically:

  • No Replay-internal failure should escape as a global unhandled promise rejection.
  • A failed full snapshot should not leave DOM mutation recording permanently locked or disabled.
  • Replay should recover, retry, or stop recording cleanly.
  • Subsequent DOM changes and SPA navigation should remain visually consistent with the Replay URL and activity timeline.
  • The originating exception and operation should be retained with an actionable stack.
  • One internal failure should not create multiple opaque Sentry issue groups.
Actual Result

Around the point where the Replay visual state stops updating, Sentry captures:

CustomEvent: Event CustomEvent (type=unhandledrejection) captured as promise rejection

and:

UnhandledRejection: Object captured as promise rejection with keys: [object has no keys]

A correlated error appears shortly afterward as a sentry.event breadcrumb:

TypeError: Cannot call a class as a function

The originating TypeError does not have a useful stack. The top-level events contain only the global unhandledrejection capture path.

After the failure:

  • The application continues operating normally.
  • The Replay URL changes when the user navigates.
  • Network, console, WebSocket, media-session, and application diagnostic events continue to be recorded.
  • The rendered Replay viewport remains permanently stuck on an earlier page.
  • No corresponding user-facing crash, disconnection, or navigation failure has been identified.

We reviewed additional Replays belonging to the same Sentry issue. Most of the reviewed Replays show this same mismatch between continuing application activity and a stale visual frame.

Additional Context

Observed scope

All sampled occurrences in this issue group were from Safari on macOS.

One representative affected Replay used Safari 26.6.2. This does not mean the issue reproduces on every Safari installation or every installation of Safari 26.6.2. The exact browser, DOM, and timing conditions required to trigger the failure remain unknown.

The same error group has appeared across multiple application releases.

Suspected Replay checkout path

This is a hypothesis based on the timing, the frozen visual state, and inspection of the bundled Replay/rrweb source. We have not confirmed the exact throwing statement.

In error-buffer mode, Replay uses a 60,000 ms checkout interval. The observed failures occur close to that boundary and can repeat at approximately 60-second intervals.

The examined full-snapshot path appears to perform the following sequence:

lock mutation buffers
serialize the document
emit the full snapshot
unlock mutation buffers

The lock/snapshot/unlock sequence does not appear to protect the unlock operation with try/finally.

If Safari throws while the document is being serialized, the mutation buffers could remain locked. This would be consistent with the observed behavior: later DOM mutations disappear from the Replay while navigation, console, network, and other non-DOM events continue.

The rrweb callback wrapper also appears to mark internal errors and rethrow them. Replay then filters internal rrweb errors from normal capture. This may explain why the potentially useful TypeError is not reported with its original stack, while a downstream CustomEvent or empty object reaches the global promise-rejection handler.

The exact Safari DOM object or serialization operation that produces TypeError: Cannot call a class as a function has not been identified because the originating stack is unavailable.

User impact

No application malfunction has been observed. The current impact is on observability:

  • Repeated noisy error events are generated.
  • Multiple opaque issue groups may represent the same underlying failure.
  • Replay becomes incomplete or misleading.
  • Later user actions cannot be reconstructed visually even though their accompanying activity remains present.

Related issues reviewed

We searched open and closed issues. The closest reports do not describe the same behavior:

The automatically suggested issue #17873 concerns beforeErrorSampling behavior and does not describe DOM recording becoming frozen.

Questions

Could the Replay team advise:

  1. Can the full-snapshot path guarantee mutation-buffer unlocking with try/finally if document serialization throws?
  2. Can Replay recover, retry, or stop cleanly after an unsuccessful periodic checkout?
  3. Can an internal rrweb exception be prevented from escaping as an opaque global promise rejection?
  4. Is there a supported diagnostic option that preserves the original rrweb exception and stack without recursively capturing Replay’s own errors?
  5. Are there known Safari DOM values or Web APIs that can produce Cannot call a class as a function during a full snapshot?
  6. Would any additional debug logging be useful for identifying the precise serializer operation?
Priority

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it.

Dominant language
TypeScript
Stars
8.7k
Forks
1.9k
Avg merge
1d 18h
Merged PRs (30d)
543

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from getsentry/sentry-javascript

All issues in getsentry/sentry-javascript

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.