Skip to content

Report global errors once with several Providers or StrictMode (#108) - #171

Open
devtools-agent[bot] wants to merge 8 commits into
mainfrom
fix/108-duplicate-global-capture
Open

devtools-agent[bot] wants to merge 8 commits into
mainfrom
fix/108-duplicate-global-capture

Conversation

@devtools-agent

@devtools-agent devtools-agent Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Fixes #108.

What was wrong

Each <Provider config={…}> builds its own Rollbar instance. Every instance with captureUncaught / captureUnhandledRejections adds its own window.onerror / unhandledrejection handler, and rollbar.js only filters duplicates within one instance. So one uncaught error or unhandled rejection was reported once per Provider. Under <React.StrictMode> a single Provider reported twice, because React constructs class components twice in development, throws one away, and the discarded instance's handlers stay installed.

What changed

Each instance a Provider creates now decides, at the moment an uncaught error or rejection arrives, whether it is the one that reports it. The app's options and configure are left alone; nothing is switched on or off.

  • Who reports. Only instances with the same accessToken are deduplicated, so Providers for different Rollbar projects (micro-frontends, say) each still get every error. Among the instances for one token that currently capture that kind of error (and aren't enabled: false), the one whose Provider was created first among those mounted reports. That Provider's own options (environment, checkIgnore, …) decide how the error is sent. Parents are created before children and earlier siblings before later ones, so the outer or first Provider reports. While none is mounted (all unmounted, or before the first commit), the newest one that has mounted reports; failing that, the newest one, which covers StrictMode's extra construction.
  • Runtime changes just work. The choice is computed from the instances' current options every time, so configure / useRollbarConfiguration opt-ins and opt-outs are respected immediately. The result doesn't depend on the order Providers mounted or unmounted in, so StrictMode and production pick the same instance.
  • src/global-capture.js (new, ~160 lines): wraps each managed instance's handleUncaughtException and handleUnhandledRejection (the methods rollbar.js's global handlers call) with that check. An instance shared by several Providers (a custom Rollbar constructor returning a singleton) is wrapped once. Every instance chooses from the same registered Providers, so they always agree on which one reports. Chrome's anonymous errors are counted by every instance as before, and the one instance whose Error.prepareStackTrace receives the error hands it to the reporting instance.
  • src/provider.js: still creates the instance in the constructor and keeps state.rollbar / state.options, so nothing changes for code that reads them. It registers the instance with the coordinator and reports mount and unmount. config is resolved with value() from utils.
  • index.d.ts: unchanged from main.
  • README.md: short note on the behaviour, including whose options apply.

Skipped during SSR (no window) and for instances passed with the instance prop.

Validation

  • src/tests/components/global-capture.test.tsx, 21 cases (React 17): sibling Providers, StrictMode, handoff on unmount, keyed remount, nesting (with StrictMode, through an instance Provider), renders thrown away by an error boundary (single and nested), a throwing constructor, no extra re-renders of nested Providers, and the runtime opt-in/opt-out cases. New in this revision: one report per project with different tokens, a shared instance from a custom constructor, state.rollbar kept, configure and options left untouched, an outer Provider that opts in, one report after every Provider unmounts, and anonymous errors in Chrome. Each test uses its own access token and resets window.onerror, so it starts from an empty page. On main, 13 of the 21 fail; on the previous head of this PR (f499118), 7 fail.
  • src/tests/global-capture.test.js, 13 cases driving the coordinator in the React 18 and 19 StrictMode mount/unmount orders, plus per-project scoping, disabled instances, shared instances, teardown, and StrictMode's extra construction not winning the fallback. New in this revision: a shared instance plus a separate instance for the same token, unmounted in either order and with a never-mounted StrictMode construction, and an instance that turns capture back on after every Provider unmounted. Each reports once; all 4 reported twice or picked the wrong instance on the previous head (d83c9ab).
  • Full suite 47/47. npm run lint, npm run typecheck, npm run build and prettier --check on the changed files are clean.
  • Built lib/ on React 18.3.1 and 19.2.0 createRoot with the rollbar 3.1.0 browser build. Each step fires one uncaught error plus one rejection, and every scenario ran with StrictMode on and off, with identical results:
Scenario This PR main
3 sibling Providers, then A and C unmount A 2 · B 0 · C 0 → B takes over each reports every error (×2 more under StrictMode)
Nested Providers outer only both
Outer (capture off) opts in over an inner capturing Provider outer only both
Tokens A, A, B A once, B once all three
Shared instance from a custom constructor, one Provider unmounts one report per error, capture still on n/a
root.unmount() with 3 Providers one report per error three (six under StrictMode)
Render error caught by a boundary below the Provider the provided instance reports n/a
B opts out, A unmounts A (unmounted) reports, B silent n/a
A (reporting) opts out B reports n/a
B opts in while A reports, then A unmounts A, then B n/a

Known limitations

  • Instances passed with the instance prop, or created with Rollbar.init, aren't coordinated; create them once and share them.
  • Copies of this package in separate bundles don't share coordination state.
  • Chrome's anonymous (cross-origin) errors only go to the project of whichever instance set Error.prepareStackTrace last, as before this change.
  • An unmounted Provider's instance keeps its handlers installed, as it always has, and reports only when no mounted Provider for its token captures that kind.

🤖 Generated with Claude Code

Every Provider built its own Rollbar instance in its constructor, and every
instance with captureUncaught / captureUnhandledRejections adds its own
window.onerror and unhandledrejection handler. rollbar.js only filters
duplicates within one instance, so one error was reported once per Provider
(#108). Under StrictMode a single Provider reported twice, because React
constructs class components twice in development and the discarded
instance's handlers stay installed.

- Create the instance on first render instead of in the constructor, so the
  constructor StrictMode discards never builds one.
- Coordinate global capture between Provider-created instances: the first
  one to render owns each kind of capture; later ones are built with it off
  and take over when the owner unmounts. The handoff flips the options
  through configure(), which rollbar.js checks on every call. Instances
  passed via `instance` aren't touched.

Fixes #108

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rollbar-circleci-machine rollbar-circleci-machine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Agent Review (openai, openai-astra)

Coordinates global error capture across Providers so that only one Provider-created Rollbar instance handles window.onerror / unhandledrejection at a time. It also moves instance creation from the constructor into the first render, so StrictMode's thrown-away constructor call doesn't build a second instance.

What I traced against the installed rollbar 3.1.0 and React 17.0.2 source:

  • configure() merges options (utility.js:791-801) and re-runs setupUnhandledCapture() (core.js:111-135).
  • A disabled instance's handlers stay on window but return early (core.js:234-240, 338-344), so turning off the previous owner stops duplicate reports.
  • The takeover, remount, nested and StrictMode paths behave as the new tests expect. I traced the tests by hand; I have not seen them run.

One issue: the instance and its claim are created during render, and a claim is only released when its Provider unmounts. If React throws away a Provider's first render, the claim stays pending forever. That instance keeps capturing global errors, and the Provider that actually mounts never takes over. Details are in the finding.

Minor, not flagged: Provider no longer sets this.state, so ref.current.state.rollbar, which the old ProviderState typing exposed, is now undefined at runtime. It was never documented API, but it is an observable change.

Comment thread src/global-capture.js Resolved
A Provider mounting next to a pending owner only waited for it, since a
pending owner could be an ancestor that mounts later in the same commit.
But if React threw that owner's render away (for example a concurrent
root re-rendering after a caught render error), it never mounts or
unmounts: its instance kept reporting uncaught errors without the person
or context set on the provided instance, and no later Provider on the
page ever took over.

Each claim now records the enclosing Provider's claim, passed down
through context. On mount, a pending owner keeps capture only if it is
an ancestor; any other pending owner is replaced like an unmounted one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rollbar-circleci-machine rollbar-circleci-machine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Agent Review (openai, openai-astra)

Reviewed the new src/global-capture.js coordination, the Provider changes, the README text and the new tests against the checkout and the installed rollbar@3.1.0 / react@17.0.2.

What holds up

  • Creating the instance in render() instead of the constructor fixes the StrictMode double instance on React 17. The throwaway instance never renders, and the double render() reuses the cached this.rollbar.
  • The PR switches capture on and off with rollbar.configure(...). In rollbar 3.1.0 a false flag overrides true (utility.js:858-866), a switched-off instance's handler returns immediately (core.js:234-240, 338-344), and switching capture back on installs the handlers (core.js:133, 208-232).
  • One Provider at a time owns capture, both across siblings (earlier renders mount first) and when nested (children mount before parents).

Issue (low)

  • If nested Providers are both inside a render React throws away (an error boundary that remounts its children, like the test's Retry), the inner Provider ends up owning global capture and the outer one waits. That contradicts the nested test, the comment in mountGlobalCapture and the README. Errors are still reported only once, but under the inner Provider's config.

Minor notes (not raised as findings)

  • If a custom Rollbar constructor throws right after its claim switched off an unmounted owner, nothing captures global errors until another Provider claims.
  • ProviderState is gone from index.d.ts; nothing in the repo references it.

The tests were not run as part of this review.

Comment thread src/global-capture.js Resolved
When a render holding nested Providers was thrown away, the inner
Provider that mounted took over global capture, because it mounts before
its parent and the stale owner wasn't its ancestor. It now hands capture
to its outermost enclosing Provider that captures the same kind, which
mounts later in the same commit.

The same thing happened with nested Providers under StrictMode on React
18 and 19 (createRoot), with no thrown-away render: StrictMode unmounts
and remounts every component after the first mount, and the owner's
unmount handed capture to its own child. An unmounting owner now skips
Providers nested inside it, since they unmount or disappear with it, and
hands capture to the outermost waiting Provider around the one it picks.

If a custom Rollbar constructor throws after its claim switched off an
unmounted owner, that owner is now switched back on instead of leaving
nothing capturing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rollbar-circleci-machine rollbar-circleci-machine left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Agent Review (openai, openai-astra)

Report global errors once with several Providers or StrictMode (#108)

The coordination is correct in every scenario I traced. I read src/global-capture.js and src/provider.js in full and walked through React 17 legacy order, React 18 StrictMode (children unmount first, then remount) and React 19 StrictMode (parents disappear first, children reappear first). I also covered siblings, nesting (including through an instance Provider), renders thrown away by an error boundary or Suspense, a constructor that throws (including React 17's DEV replay), and interleaved commits across multiple roots. Two things held throughout:

  • At most one managed instance has each kind of capture enabled at a time.
  • Every path that disables an owner also enables a replacement (takeOver, or the unmounted-owner branch of claimGlobalCapture), and abandonGlobalCapture puts the previous owner back.

Switching capture through configure works in the pinned Rollbar (3.1.0). configure re-runs setupUnhandledCapture (node_modules/rollbar/src/browser/core.js:111-135, :208-232). handleUncaughtException and handleUnhandledRejection check the options each time they run (core.js:234-240, :338-344). The window.onerror chaining (node_modules/rollbar/src/browser/globalSetup.js:1-27) explains the original duplicate reports. The peer range also allows rollbar ^2.26.4, which isn't installed here, so I couldn't check 2.x.

Behaviour changes worth noting (none of them defects):

  • Several Providers with different access tokens no longer all get uncaught errors. Only the owner reports them. This is what #108 asks for, and the README documents it.
  • Provider no longer has this.state, at runtime or in index.d.ts. Code that read ref.current.state.rollbar will break. That was undocumented, and nothing in this repo uses it.
  • The ownership state is module-level, so it only coordinates Providers that share one copy of the package.

Outside the changed files: the experimental, unexported useScopedConfiguration (src/hooks/use-scoped-rollbar-config.js:18) builds a new instance from base.options. Used under the owner, that instance would capture global errors too.

One finding (low): the new static contextType makes nested Providers re-render more often (see below).

I did not run the tests; nothing here shows whether they pass.

Comment thread src/provider.js Resolved
Provider read the enclosing Provider's claim from `Context`, whose value is
a new object on every render. That subscribed each nested Provider to its
parent, so a parent re-render forced every inner Provider, and the
`useRollbar` consumers under it, to re-render as well.

The claim now travels in a private context whose value is the claim itself,
which doesn't change after a Provider's first render. `Context` carries what
it did before this branch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rollbar-circleci-machine

Copy link
Copy Markdown
Contributor

AI Agent Review (openai, openai-astra)

Review: global capture coordination across Providers (#108)

No defects found in the changed lines. I checked the mechanism against the actual code and the pinned rollbar==3.1.0 (package-lock.json):

  • The handoff through configure() works. In node_modules/rollbar/src/browser/core.js, configure() (111-135) merges options and calls setupUnhandledCapture() (208-232). That function installs handlers the first time a capture flag is true. Both handleUncaughtException (234-240) and handleUnhandledRejection (338-344) check the capture options each time they fire. So disable() silences an instance whose handler stays installed, and enable() either installs a handler late or turns an existing one back on.
  • Handlers aren't lost. globalSetup.js captureUncaughtExceptions (1-27) adds the new handler in front of the existing window.onerror and still calls the old one. captureUnhandledRejections (54-97) only removes the previous listener when it belongsToShim. So when capture goes back to an earlier owner, its handler is still attached.
  • The state machine holds. I traced React 17 (parents unmount first), React 18/19 StrictMode double-invoked effects, thrown-away renders (error retry, Suspense on first mount, Activity pre-render), a failing constructor (abandonGlobalCapture) and the hide/show cycle. Each time, a mounted capturing claim is either the owner or in waiting, and takeOver only turns off claims that aren't mounted. The four direct tests in src/tests/global-capture.test.js match my manual traces.
  • Lazy creation in render(). StrictMode's double construction keeps one object and calls render() twice on it, so getRollbar() builds one instance. The CaptureClaimContext value only changes on a Provider's first render, so nested Providers don't re-render when the outer one does.

I did not run the test suite or lint. Nothing here claims they pass.

Worth deciding deliberately (not defects in this diff)

  • Behaviour change for multi-project apps. If several Providers use different accessTokens (for example micro-frontends reporting to separate projects), global errors now go only to the owning Provider's project, not to each one. The README change documents this; a changelog or semver note may be warranted.
  • Provider#state is gone. index.d.ts no longer types state.rollbar / state.options, and at runtime this.state is now null. Anything that read the instance through a ref to Provider breaks. This was never documented API, but it is visible in the old typings.
  • Outside the changed files: src/hooks/use-rollbar-config.js:5-8 and src/rollbar-configuration.js:25-38 call rollbar.configure(config) directly. If that config includes captureUncaught or captureUnhandledRejections, capture gets turned back on for a non-owner instance and bypasses this coordination.
  • Module-level state: owners and waiting live in one module. If an app bundles two copies of @rollbar/react (for example both the lib and dist builds), each copy coordinates separately.

@brianr
brianr requested a review from waltjones October 1, 2026 05:46
@linear-code

linear-code Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

SDK-700

CR-143

@aborek-rollbar

Copy link
Copy Markdown

[P2] Preserve runtime capture settings during ownership handoff

enable() restores claim.captures[kind], which contains the Provider’s initial capture settings. This overwrites subsequent changes made through the public useRollbarConfiguration() hook.

Reproduction:

  1. Mount sibling Providers A and B, both configured with global capture enabled.
  2. Inside B, call:
    useRollbarConfiguration({
      captureUncaught: false,
      captureUnhandledRejections: false,
    });
  3. Unmount A while keeping B mounted.
  4. The handoff calls enable(B, kind) and restores both flags to true. B starts reporting global errors again despite the explicit opt-out.

The coordinator should distinguish the application’s current requested settings from its own temporary suppression of capture, and preserve those requested settings during a handoff.

@waltjones waltjones left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking on the issue @aborek-rollbar raised.

What I ran: I used the PR's built package on React 18.3.1 and 19.2.0, without StrictMode, which matches production. The main column comes from reading the code, not from running it.

┌────────────────────────────────────────────────────────────────┬───────────────────────────────────────────────────────────────────────────────┬─────────────────────────────────────┐
│                              Case                              │                                    PR 171                                     │                main                 │
├────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────┤
│ Reviewer's repro: B opts out with useRollbarConfiguration,     │ B reports both errors, despite the opt-out, until the component that opted    │ B stays silent; A's instance keeps  │
│ then A unmounts                                                │ out happens to re-render                                                      │ reporting                           │
├────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────┤
│ The capturing Provider (A) opts out while B is mounted         │ Nothing reports. B never takes over                                           │ B reports                           │
├────────────────────────────────────────────────────────────────┼───────────────────────────────────────────────────────────────────────────────┼─────────────────────────────────────┤
│ B, not capturing, opts in at runtime                           │ The uncaught error is reported twice, by A and B                              │ Twice (the existing bug)            │
└────────────────────────────────────────────────────────────────┴───────────────────────────────────────────────────────────────────────────────┴─────────────────────────────────────┘

With StrictMode in development it gets more confusing. StrictMode unmounts and remounts everything once after mounting, and that runs the handoff code. The handoff puts every Provider back to its original capture settings, which undoes any opt-in or opt-out made during the first render. So development and production behave differently.

Why it happens: the coordinator saves the capture settings when the Provider is created (claim.captures) and switches them through configure(). The app changes the same two options through the same configure(), using the public useRollbarConfiguration (typed at index.d.ts:79) or useRollbar().configure(...). Neither side sees the other's changes, so whichever writes last wins.

Severity: P2 is fair. Apps that set the capture flags once in config are fine. Still, an opt-out that's silently undone matters, since an app may turn capture off on purpose. In the second row, global errors stop being reported at all. The fix only touches global-capture.js and provider.js, so I'd do it in this PR rather than as a follow-up.

Suggested fix: let the coordinator own the capture flags on the instances Providers create, by wrapping each instance's configure:

  • Record requests: capture keys in an app's configure call become that Provider's requested settings, replacing what was saved at creation.
  • Apply only when owning: apply them only if that Provider currently captures that kind of error; otherwise keep them off.
  • Opting out: a capturing Provider that opts out hands capture to the next waiting Provider, the same as on unmount. A waiting Provider that has opted out is skipped at handoff.
  • Internal switching: enable and disable call the original configure, not the wrapped one.
  • Providers that start without capture: they don't get a claim today (claimGlobalCapture returns null), so a later opt-in on one would still double-report. They need a claim too.

Fixing only the hook isn't enough, because useRollbarConfiguration is just a two-line wrapper around rollbar.configure. A direct call would hit the same bug.

The three rows above would make good regression tests in src/tests/components/global-capture.test.tsx. React 17 can reproduce all of them without StrictMode. They're also in my harness at /private/tmp/claude-501/-Users-waltjones-projects-rollbar-rollbar-react/797d7152-fa2f-447d-898e-57a72dbe421f/scratchpad/harness/run.cjs, as runtimeOptOut, ownerOptOut and runtimeOptIn.

The coordinator saved each Provider's capture options when it built the
instance and restored them on every handoff, while the app changes the
same options through configure() or useRollbarConfiguration(). Whichever
wrote last won: an opt-out was undone when the owner unmounted, an owner
that opted out left nothing capturing, and a Provider that opted in
reported every error a second time.

Each managed instance's configure is now wrapped. Capture options passed
to it become what that Provider wants, and are applied only while it owns
that kind of capture. An owner that opts out hands capture to the next
mounted Provider that wants it, Providers that opted out are skipped at
handoff, and a Provider that opts in captures only once nobody else does.
Internal switching calls the original configure. Providers that start
without capture now get a claim too, so a later opt-in is coordinated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devtools-agent

Copy link
Copy Markdown
Author

Addressing the CHANGES_REQUESTED review from waltjones and the P2 comment from aborek-rollbar (runtime capture settings undone at handoff). Fixed in f798951, following the suggested design.

What changed (src/global-capture.js, src/provider.js)

  • Requests are recorded. Each instance a Provider creates gets its configure wrapped (manageGlobalCapture). Capture keys passed to it become that Provider's requested settings (claim.requested), replacing what was saved at creation. The other options pass through unchanged.
  • Applied only when owning. Requested capture settings take effect only while that Provider owns that kind of capture. Otherwise the instance keeps it off.
  • Opting out. An owner that opts out hands capture to the next mounted Provider that wants it, or leaves it with nobody if none does. A waiting Provider that opted out is skipped at handoff, including on unmount.
  • Opting in. A Provider that opts in captures only when no other Provider does (no owner, or the owner has unmounted).
  • Internal switching. enable/disable call the original configure, so the coordinator's own switching doesn't count as an app request.
  • Providers without initial capture. They now get a claim too, so a later opt-in is coordinated.
  • Because handoff restores requested settings instead of the creation-time ones, opt-ins and opt-outs made during the first render survive StrictMode's unmount and remount. Development and production now behave the same.

Regression tests. The three rows of the table are now in src/tests/components/global-capture.test.tsx, and all three fail on d1f9094. A fourth test covers the only owner opting out and back in. src/tests/global-capture.test.js gains React 18/19 StrictMode-order cases: an opt-out or opt-in made during render surviving a remount, and an owner opting out handing capture to the outer of nested Providers. Full suite 35/35; npm run lint, npm run typecheck, npm run build and prettier --check on the changed files are clean.

Built lib/ on React 18.3.1 and 19.2.0 createRoot, StrictMode on and off (one uncaught error plus one rejection per run, reports per instance):

Case d1f9094 f798951
B opts out with useRollbarConfiguration, then A unmounts B: 2 A: 2 (unmounted owner keeps it, as on main)
The owner A opts out while B is mounted nothing reported B: 2
B, started without capture, opts in A: 2, B: 2 A: 2; after A unmounts, B: 2

All four modes give the same numbers. The earlier scenarios are unchanged: three StrictMode siblings (A reports, then B after A and C unmount), nested Providers, and a nested render thrown away by a boundary above them (the outer instance reports).

Not covered: capture options assigned to rollbar.options directly, rather than through configure, aren't seen. The experimental, unexported RollbarConfiguration component restores its parent options on unmount by passing the whole old options object to configure. For a Provider that isn't capturing, that object has capture off, so it counts as an opt-out. Both are in the PR description.

@devtools-agent

Copy link
Copy Markdown
Author

AI Agent Review (openai, openai-astra)

No defects found; I did not run the tests.

Checked against the pinned rollbar 3.1.0 (package-lock.json):

  • Handoff works. The handlers re-check the capture options on every event (node_modules/rollbar/src/browser/core.js:235-240, :339-343).
  • Late enable works. configure re-runs setupUnhandledCapture (core.js:133, :208-232), so an instance built with capture off installs its handler when the coordinator turns capture on.
  • Old handlers stay harmless. Each new handler chains to the previous window.onerror (node_modules/rollbar/src/browser/globalSetup.js:7-26); a switched-off instance stays in the chain but does nothing.
  • Configs without capture options are safe. Rollbar's defaults leave capture off (core.js:588-607).

Coordinator logic (src/global-capture.js): every path that turns an instance on also makes it the owner and turns the previous owner off: takeOver (:58-65), claimGlobalCapture (:146-162), reconcile (:107-126), handOff (:92-104), abandonGlobalCapture (:250-266).

Scenarios traced by hand, all with exactly one instance capturing: sibling Providers; nested Providers, including through an instance Provider; StrictMode on React 17, 18 and 19; a key remount; a route change that unmounts and mounts Providers in the same commit; Suspense or Activity hiding and revealing a subtree; a render thrown away before commit; a constructor that throws; capture turned on or off at runtime, during render and in effects.

src/provider.js: the instance is built on first render (:87-112), which works because StrictMode's double render reuses the same component instance. The private claim context (:26, :132-134) holds the same value after the first render, so nested Providers don't re-render with the outer one. The in-repo callers of configure pass no capture options, so the wrapper doesn't affect them (src/history-context.js:37, src/rollbar-context.js:33,53, src/hooks/use-rollbar-context.js:18-20, src/hooks/use-rollbar-config.js:7).

Context (not findings):

  • Only rollbar 3.1.0 was checked. package.json:109 also accepts rollbar ^2.26.4 and ^4.0.0, which aren't installed.
  • Intentional behaviour change. Sibling Providers with different access tokens now send uncaught errors only to the project of the first one to render. This is what Request is sent to Rollbar several times #108 asks for, and README.md:203-207 documents it.
  • Already declared in the PR description: providerRef.current.state.rollbar no longer exists (matches the index.d.ts change); the unexported RollbarConfiguration restores old options in a way that counts as an opt-out (src/rollbar-configuration.js:29-37).
  • Outside the diff, not new: the unexported useScopedConfiguration still builds an uncoordinated instance from base.options (src/hooks/use-scoped-rollbar-config.js:18).

waltjones
waltjones previously approved these changes Oct 26, 2026
Comment thread src/global-capture.js Resolved
Comment thread src/global-capture.js Resolved
Comment thread src/provider.js Resolved
Comment thread src/global-capture.js Resolved
Comment thread src/global-capture.js Resolved
Comment thread src/global-capture.js Resolved
Comment thread src/global-capture.js Resolved
Comment thread src/global-capture.js Resolved
Comment thread src/provider.js Resolved
Addresses the review on #171. Instead of switching capture on and off
through configure, each instance a Provider creates now checks, when its
uncaught-error or rejection handler runs, whether it is the one to report:
of the instances with the same access token that capture that kind, the
first-created mounted one; with none mounted, the newest that has mounted,
else the newest. The choice is made from current options each time.

- Scoped per accessToken, so Providers for different projects each report.
- configure and the app's options are untouched; state.rollbar is kept
  (instance is created in the constructor again; ProviderState restored).
- A shared instance from a custom constructor is managed once.
- Same owner with or without StrictMode; nothing is enabled at teardown.
- Chrome anonymous errors reach the reporting instance.
- Component tests use a token per test so each starts from a clean page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@devtools-agent devtools-agent Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Agent Review (anthropic, CLI default model; requested openai/openai-astra)

Scrutiny: high (failure path)

Before this change, an app with more than one <Provider config={…}> that captures uncaught errors or unhandled promise rejections reported each crash once per Provider, so one crash showed up as two, three or more occurrences. In development, <React.StrictMode> caused even a single Provider to report twice, because React builds a throwaway extra copy of it.

After this change, Providers that report to the same project (the same access token) pick one of themselves to report each error. That is the outermost or first-rendered Provider still on the page. If none is on the page any more, the most recent one that was takes over, so errors are still reported after the app unmounts. Turning capture on or off while the app runs takes effect from the next error.

Situation Before After
Several Providers, same project one report per Provider one report
One Provider under StrictMode (development) two reports one report
Providers for different projects (e.g. micro-frontends) one report each one report each (unchanged)
Provider given a ready-made instance, or Rollbar.init not coordinated not coordinated (unchanged)
Server rendering no capture no capture (unchanged)

This also changes which settings apply. For global errors, only the chosen Provider's own settings decide whether and how the error is sent: its environment, context, checkIgnore filter and so on. An inner Provider configured with a more specific context or a looser filter no longer sends its own copy. That is the intent of the change, but a sentence about it in the README would save users some surprise.

Conclusion: I found no blocking problem. There is one low-severity gap. In a mixed setup, a single error can still be reported twice once the Providers involved have all left the page. The setup is one Rollbar instance shared by several Providers through a custom constructor, plus another Provider for the same project that builds its own instance. The inline comment walks through it.

How I reached this. I read the new coordination module, the Provider changes, and the global-error code of rollbar.js 3.1.0, which is the version pinned in package-lock.json. Rollbar installs a separate handler for each instance and chains them, so every instance sees every error. Each handler is looked up on the instance at the moment the error happens, which is why wrapping it per instance works. I traced these situations by hand through the code:

  • sibling and nested Providers
  • StrictMode's extra copy, and the unmount-then-remount order of React 18 and 19
  • a render thrown away by an error boundary
  • disabled instances
  • turning capture on or off at runtime
  • different access tokens
  • one shared instance
  • everything unmounting
  • Chrome's anonymous cross-origin errors

Apart from the mixed case above, each gave exactly one report per project. The common case of a single Provider in production behaves as before.

The description lists some known limitations, with no pull request or ticket behind them. The code confirms each one matches how things worked before this change, so none is a regression. They are: Providers given an instance, separate copies of the package in different bundles, and Chrome anonymous errors going to only one project.

One remaining risk, which I did not raise as a finding because it needs a misbehaving third-party script: this approach assumes every Provider's handler is still being called. Suppose a script replaces window.onerror without passing errors on. Providers created after that would defer to an earlier Provider that no longer receives errors, and the error would be lost. Before this change, those later Providers reported it themselves.

Limits: I read the new tests but did not run them. I could not check the React 18/19 browser results in the description beyond reading the unit tests that simulate those mount orders.

Comment thread src/global-capture.js Resolved
A shared instance (custom Rollbar constructor) and a separate instance
for the same token could both report an error once all their Providers
had unmounted. Each instance added its own newest claim to the
candidates, and unmounting dropped the newer claims of a shared instance
in favour of the one just unmounted, so the two instances disagreed.

- reporter() chooses only from the registered claims, so every instance
  reaches the same answer; an instance with none registered reports only
  when no other instance for its token does.
- On unmount, an instance keeps its newest claim that has mounted, so the
  result no longer depends on unmount order and StrictMode's extra
  construction can't stand in for it.
- README: the reporting Provider's options decide how the error is sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Request is sent to Rollbar several times

4 participants