regression: skip "ResizeObserver loop" warning in Bugsnag reports

Merged
RI
5mo ago
+8-0

Proposed changes (including videos or screenshots)

While scrolling a channel, Bugsnag was being flooded with error events whose message is:

ResizeObserver loop completed with undelivered notifications.

This is not a real error - it is a benign, informational browser signal (per the W3C Resize Observer spec) dispatched as an ErrorEvent for historical reasons, which makes error trackers misclassify it as an unhandled exception. It became visible in 8.5.0 because PR #40105 virtualized the message list using virtua, whose per-item ResizeObservers fire in bursts during scroll and flood Bugsnag. (What the warning means and how it manifests is detailed in Further comments.)

This PR adds an onError callback to Bugsnag.start() that drops events whose top error message starts with ResizeObserver loop. The warning still occurs in the browser (developers can still see it in the console); we only stop reporting a benign browser signal as a crash. Maintainers of comparable virtualization libraries recommend the same: in TanStack/virtual#531 - a different library than virtua, but the same per-item ResizeObserver pattern and the same warning - a collaborator recommends ignoring/filtering this exact message rather than trying to fix it (details in Further comments).

onError(event) {
  const errorMessage = event.errors?.[0]?.errorMessage;
  if (typeof errorMessage === 'string' && errorMessage.startsWith('ResizeObserver loop')) {
    return false; // benign browser signal, not a crash
  }
}

String matching is unavoidable: the event has no usable stack (lineNumber: 0) and ErrorEvent.error is null, so the message string is the only identifier the browser exposes. startsWith('ResizeObserver loop') also covers Safari's older variant, ResizeObserver loop limit exceeded.

This is a deliberate, palliative fix. It stops the Bugsnag noise immediately with zero performance cost, but it does not remove the warning itself - the root cause is virtua's per-item ResizeObservers. We filter for now because the source-level fixes all trade performance or maintenance for it (see Further comments); a proper non-regressing fix stays open for later.

Issue(s)

CORE-2209

Steps to test or reproduce

  1. Open a channel with a few hundred messages (so the list virtualizes).
  2. Enable Bugsnag locally: start the server with BUGSNAG_CLIENT=00000000000000000000000000000000 (a dummy 000 works - requests return 401, which is fine for this test).
  3. Open DevTools -> Network, filter for notify.bugsnag.com.
  4. Scroll the message list up and down aggressively.
  • Before this change: repeated POSTs to notify.bugsnag.com carrying the ResizeObserver loop message (up to Bugsnag's per-session maxEvents: 10 cap, after which further events are dropped client-side).
  • After this change: 0 POSTs to notify.bugsnag.com for the ResizeObserver loop message during the same scroll. As a control, a genuinely thrown Error still POSTs normally - confirming the filter drops only the targeted message.

Further comments

What the warning means and how it shows up in our app

ResizeObserver delivers size-change callbacks at most once per animation frame. The browser runs the observer callbacks, then checks whether those callbacks caused new size changes; if they did and it can't deliver them within the same frame, it defers the rest to the next frame and emits this warning. In plain terms: "a resize callback changed layout, which produced more resizes than I could deliver this frame - I postponed the rest." Nothing is lost; the deferred notifications arrive on the next frame, which is why it is benign.

In our app it manifests during message-list scrolling, roughly like this:

  1. You scroll -> virtua mounts the message rows entering the viewport and unmounts those leaving it.
  2. virtua's internal ResizeObserver measures each newly mounted row.
  3. Those measurements feed back into virtua's layout (item offsets, total size, spacer height), which it adjusts - and that adjustment, happening inside the same delivery cycle, produces new size observations.
  4. When a scroll burst mounts/adjusts enough rows in one frame, the browser can't deliver them all, so it defers and fires the warning.

It is triggered two ways, both real:

  • Plain-text messages - by volume. Even with stable-height text, fast scrolling mounts many rows at once; the sheer number of first measurements plus virtua's feedback in a single frame overruns the per-frame budget. We verified this directly: a text-only channel (no images, no code blocks) still produced the warning in bulk.
  • Images / code blocks - by magnitude. A row whose height changes after it was first measured - an image finishing loading, or a code block laid out via dangerouslySetInnerHTML - forces a large re-measurement, the worst case for the loop. These amplify the rate but are not required for it.

useGetMore adds a spike at the top of the list: scrolling up loads older history and prepends a batch of messages at once, causing a burst of simultaneous mounts and measurements in a single frame.

Why filter instead of fixing the warning at the source?

We confirmed empirically (instrumenting window.ResizeObserver and attributing each warning to the observers that fired in the same frame) that the dominant generator is virtua's own internal ResizeObservers - not our code. virtua exposes no API to inject a custom/deferred observer; it reads ResizeObserver off the element's window at observe-time.

This is also what the maintainers of TanStack recommend. In TanStack/virtual#531, a TanStack Virtual collaborator recommends ignoring/filtering the warning rather than fixing it:

I would recommend just to ignore this error, fixing it will not change anything, saying from
experience as living with this in production for a few years now.

…suggesting the exact message-based filter we use here (ResizeObserver loop completed with undelivered notifications. / ResizeObserver loop limit exceeded), and explicitly noting that "patching it with requestAnimationFrame will not solve the underlying issue" - which is why we did not pursue the rAF shim below.

The related perf PR does not fix this. #40695 replaces flushSync with startTransition and wraps our useGetMore callbacks in requestAnimationFrame. Tested with and without it under an identical scripted scroll, the warning count is unchanged. As established above, the warnings come from virtua's observers, not our hook, so cleaning up our hook cannot move the count.

The obvious source-level fix degrades performance. Wrapping window.ResizeObserver so every callback defers one frame breaks the loop precondition, but adds ~16ms latency to every ResizeObserver in the app (~25 use sites incl. interactive ones like drag and composer auto-grow) to fix what is only a reporting problem. TanStack Virtual exposes exactly this as an option useAnimationFrameWithResizeObserver, but its own docs say it "typically should not be enabled" - ~16ms with no performance benefit, and the warning "usually indicates a deeper issue that should be fixed." A true upstream fix in virtua isn't readily available either.

Also, https://github.com/inokawa/virtua/issues/470 discuss same thing.

In short: a no-cost stopgap now; a source-level fix stays open for when one exists that doesn't cost
performance.

Follow-up task: CORE-2264

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced error reporting by filtering out non-critical ResizeObserver loop warnings, reducing noise in error logs and notifications.

Review Change Stack