Skip to content

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

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

devtools-agent[bot] wants to merge 6 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={…}> built its own Rollbar instance in its constructor. 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: React constructs class components twice in development and throws one away, but that discarded instance's handlers stay installed.

What changed

  • src/provider.js: the instance is now created on first render, not in the constructor. StrictMode's discarded constructor call never builds one (double render runs on the same component instance, so it reuses the one it has). This replaces the old TODO: use isUncaught to filter if this is 2nd Provider added.
  • src/global-capture.js (new): coordinates global capture between Provider-created instances.
    • For each kind of capture (uncaught / rejections), the first Provider to render owns it. Later Providers build their instance with that capture off.
    • When the owner unmounts, the next mounted Provider takes over. The handoff uses configure({ captureUncaught: … }), which works because rollbar.js checks those options on every call.
    • If the owner's render was thrown away before commit (for example a createRoot re-render after a render error, or an error boundary or Suspense retry above the Provider), the Provider that does mount takes over. Each claim records the enclosing Provider's claim, passed down through a private context whose value is the claim itself, so nested Providers don't re-render when their parent does. The public Context carries only what it did before. When a Provider mounts, an owner that hasn't mounted yet keeps capture only if it is an ancestor, because React mounts children before their parents. Any other owner that hasn't mounted belongs to a thrown-away render and is replaced.
    • If nobody is waiting, the unmounted owner keeps capturing, the same as an unmounted Provider's instance always has. The next Provider to claim capture turns it off, so a remount (for example after a key change) moves capture to the new instance.
    • Nested Providers: the outer one keeps capture, including through an instance Provider in between. A Provider that replaces a stale owner hands capture to its outermost enclosing Provider that captures the same kind, which mounts later in the same commit. An owner that unmounts skips waiting Providers nested inside it, because they unmount (or, under StrictMode and Suspense, disappear and reappear) along with it. It hands capture to the outermost waiting Provider around the one it picks. Without this, StrictMode on React 18/19 moved capture to the inner Provider when it unmounted and remounted everything.
    • If a custom Rollbar constructor throws, the owner its claim switched off is switched back on.
    • The app can change the capture options at runtime through configure() or useRollbarConfiguration(), so each instance a Provider creates has its configure wrapped. Capture options passed to it become what that Provider wants, and are applied only while it owns that kind of capture; otherwise they stay off. An owner that opts out hands capture to the next mounted Provider that wants it, or to nobody if none does. Providers that opted out are skipped at handoff. A Provider that opts in captures only once no other Provider does. The coordinator's own switching calls the original configure, so it doesn't count as a request. Providers that start without capture get a claim too, so a later opt-in is coordinated. Opt-ins and opt-outs made during the first render now survive StrictMode's unmount and remount, so development and production behave the same.
    • Skipped during SSR (no window) and for instances passed with the instance prop.
  • index.d.ts: removed the internal ProviderState generic, because the Provider no longer keeps the instance in state. This is observable: providerRef.current.state.rollbar is now undefined. It was never documented; useRollbar() and Context are the supported ways to get the instance.
  • README.md: short note on both behaviours.

Validation

  • New src/tests/components/global-capture.test.tsx covers 15 cases: sibling Providers, StrictMode, handoff on unmount, keyed remount, nesting under StrictMode, nesting through an instance Provider, a Provider whose first render is thrown away by an error boundary above it, the same with nested Providers, a Provider whose constructor throws, a nested Provider not re-rendering when the outer one does, which instances have capture off, and four for capture options changed at runtime: a Provider that opted out stays off when the owner unmounts, the owner opting out hands capture to the next Provider, a Provider that opts in stays off while another owns capture, and the only owner opting out and back in. The first six fail on main (e.g. 3 reports from 3 Providers, 2 constructions under StrictMode) and pass with the fix. The thrown-away-render case failed on this PR's first commit: the discarded instance sent both reports and the provided one none. The nested thrown-away and throwing-constructor cases failed on the second commit (1b7a5b4). The re-render case failed on the third (f7d66fa) and passes on main. The first three runtime-option cases failed on the fourth (d1f9094).

  • New src/tests/global-capture.test.js drives the coordinator directly in the order React 18 and React 19 StrictMode unmount and remount components, which the React 17 suite can't produce. It also covers the unmount handoff with nested Providers. The first 4 cases fail on 1b7a5b4. 3 more cover an opt-out or opt-in made during the first render surviving a StrictMode remount, and an owner opting out handing capture to the outer of nested Providers.

  • Full suite: 35/35 passing. npm run lint, npm run typecheck and npm run build are clean, and so is prettier --check on the changed files. Repo-wide it still flags 12 files in examples/ and rollup.config.mjs, the same on main.

  • The repo's tests run React 17, so I also checked the built lib/ with React 18.3.1 and 19.3.0 (createRoot + StrictMode, which also runs mount effects twice, 3 Providers, rollbar 3.1.0 browser build, one uncaught error plus one rejection):

    • This branch: 3 instances built, 2 reports total, all from the first Provider. After unmounting Providers A and C, B reports both.
    • @rollbar/react@1.0.0 from npm: 6 instances built, 12 reports.
  • Also with React 18.3.1 and 19.3.0 createRoot, nested Providers with StrictMode on and off, with and without a render thrown away by a boundary above them: the outer instance that mounted sends both reports in all 8 runs. On 1b7a5b4 the inner one did in 6 of 8, including StrictMode with no thrown-away render.

  • Also with React 18.3.1 and 19.3.0 createRoot: a StrictMode Provider whose child's render error is caught by a boundary below it (React throws away that render and renders again). The first commit on this branch built 2 instances, the discarded one sent both reports, and the provided one sent none. Worse, every Provider mounted on the page afterwards also stayed off. With the fix, the provided instance sends both reports and later Providers coordinate normally.

  • Runtime capture options, with the built lib/ on React 18.3.1 and 19.2.0 createRoot, StrictMode on and off, one uncaught error plus one rejection:

    • B opts out with useRollbarConfiguration, then A unmounts: A (unmounted, nobody else wants capture) sends both reports. On d1f9094, B sent them despite its opt-out.
    • The owner A opts out while B is mounted: B sends both. On d1f9094, nothing was reported.
    • B, started without capture, opts in: A sends both, B none; after A unmounts, B sends both. On d1f9094, both reported each error.
    • All four modes give the same result, so StrictMode no longer differs from production here.

Known limitation

When a render that creates the owning instance is thrown away, that instance keeps its (disabled) handlers installed, because nothing unmounts it. It captures only until a Provider mounts, usually the retry of the same Provider, which then takes over. So while a first mount is still suspended, errors are reported once but by that instance, without person or context from hooks. Before this change, that case created an extra instance that reported every error again.

Capture options set by assigning to rollbar.options directly, rather than through configure, aren't seen by the coordinator. 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 would count as an opt-out.

🤖 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 Outdated
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
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 Outdated
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).

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