Fix RollbarContext without a payload (#102), make onRender work and document it (#88) - #166
devtools-agent[bot] wants to merge 6 commits into
Conversation
…ocument it (#88) #102: rollbar.js has no default `payload` option, so RollbarContext and useRollbarContext threw "Cannot read properties of undefined (reading 'context')" unless the config set one. Read it with `?.`. Restoring the unset context on unmount now uses '' instead of undefined, which configure() ignores, so the context used to stay set after unmount. rollbar.js sends '' for an unset context anyway. #88: by default the context is set on mount, after the children have rendered, so errors an ErrorBoundary catches during the first render are reported with the previous context. `onRender` sets it first, but it was undocumented and had bugs: - it called setState during render, which React warns about - it ignored later changes to the `context` prop The previous context is now kept on the instance. componentDidUpdate applies a changed `context` under onRender. The README documents onRender, when to use it and why it isn't the default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests were plain JS only because index.d.ts on main had no `onRender`. Stacked on #162 they type-check as TSX, like the other component tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6f02701 to
e5456cd
Compare
|
[P2] Preserve the innermost context when an outer context updates — With nested |
Review feedback: with nested onRender contexts, changing the outer `context` prop overwrote the inner one that was still mounted. React runs update lifecycles child-first, so the outer component applied its value last. Nesting was already broken without onRender and in the hook: children mount first, so the outer context won at mount, and unmounting restored in the wrong order, leaving the inner context set. The component and the hook now share a list of active contexts per Rollbar client (src/context-stack.js). Each entry takes an order number on first render, which ranks parents before children, and the innermost active entry is applied on every change. When the list empties, the context from before the first entry is restored. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Thanks, confirmed and fixed in d722ee7. This PR caused the
Fix: the component (with or without
Regression test:
🤖 Generated with Claude Code |
CI's examples lint (eslint-plugin-react-hooks 7) rejects the hook
mutating the entry object it kept in useState ("Cannot modify local
variables after render completes"). The stack now maps each order
number to its context, and callers pass the context in, so nothing
held by React is mutated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Follow-up: CI failed on d722ee7. The examples lint (eslint-plugin-react-hooks 7) flagged the hook for mutating an object it kept in 🤖 Generated with Claude Code |
rollbar-circleci-machine
left a comment
There was a problem hiding this comment.
AI Agent Review (openai, openai-astra)
Review of #166: one medium-severity bug. The payload crash fix, the context stack and the hook rewrite are correct. With onRender, however, an error that an ErrorBoundary catches on first render leaves an entry in the context stack that is never removed, and that entry blocks every context set before it. Tests were not run.
What checks out:
- Missing
payload(#102): rollbar.js 3.1.0merge()skipsundefinedvalues (node_modules/rollbar/src/utility.js:865). So the old restore toundefineddid leave the context in place, and restoring withstack.base ?? ''fixes it.options.payload?.contexthandles a config with nopayload. - Hook: splitting
useRollbarContextinto a set effect ([ctx]) and a remove-only cleanup ([]) is correct on update and unmount, and works with React 18 StrictMode's unmount/remount.useRollbar()andgetRollbarFromContextreturn the same instance, so the class and the hook share one stack. - Class component: keeping
order/activeon the instance avoids callingsetStateduring render. ResettingactiveincomponentWillUnmountmakes StrictMode's simulated remount apply the context again. - Ordering: order numbers are handed out during render, parents before children, so "innermost wins" holds even though children mount first.
Finding: with onRender, the stack entry is added inside render() and removed only in componentWillUnmount. When the ErrorBoundary catches an error from a child's first render, React throws the RollbarContext away without committing it. The entry is never removed and outranks every context set before it, for the life of the client (details inline).
Outside the findings: during server rendering componentWillUnmount never runs. If an app passes a shared, module-level Rollbar instance to Provider, each server render of an onRender RollbarContext would add another entry that is never removed. The examples in this repo use the per-Provider config prop, so this depends on how an app is set up.
Review feedback: with onRender, the stack entry was added in render() and only removed in componentWillUnmount. When an ErrorBoundary around the RollbarContext catches an error from a child's first render, React never mounts the RollbarContext, so the entry stayed in the stack for good and outranked every other context. The same happened on the server, where nothing mounts. onRender now sets the context directly in render(), and the component joins the stack on mount. A microtask queued from render() applies the context of whatever is mounted again. React reports the error during the commit, before the microtask runs, and rollbar.js captures its options when rollbar.error() is called, so the report keeps this context. This also fixes the leak on main when no other context is mounted. The README now recommends putting RollbarContext outside the ErrorBoundary: React removes everything inside the boundary before the error is reported, so a RollbarContext inside it can't apply to errors after the first render. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AI Agent Review (openai, openai-astra)This PR replaces the per-component save/restore of I traced the stack logic against React 17's commit order: nesting order, mount before unmount, key-change remounts, discarded renders, and One gap remains, reported inline and low severity. With Other notes, not findings:
|
AI Agent Review (openai, openai-astra)Review: RollbarContext stack, the no-payload fix (#102) and onRender (#88)No defects confirmed on the changed lines. I read What I checked and found correct
Observations, not findings (low impact, or outside the changed lines)
|
Restoring an unset context as '' is only equivalent to leaving it unset in the browser. On the server, rollbar.js derives the context from the Express route in addRequestData, and addPayloadOptions then merges payload.context, including '', over it. So after an onRender server render with a shared instance whose config has no payload.context, later request errors lose their route context. rollbar.js has no way to remove the key (configure() skips undefined and the notifier keeps its own merged copy), so correct the comment and document it in the README instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #102. Covers the behaviour and docs half of #88; the typings half (
contextrequired,onRenderadded toindex.d.ts) is in #162, so #88 can be closed once both have merged.#162 has merged, and this PR now targets
main.Why
#102:
Cannot read properties of undefined (reading 'context'). Neither rollbar.js 2.26.4 nor 3.1.0 sets a default for thepayloadoption.RollbarContextanduseRollbarContextboth readrollbar.options.payload.context, so either one throws on mount unless the config happens to setpayload. While testing the fix I found a second problem with the same cause: on unmount they restore the previous context withconfigure({ payload: { context: undefined } }). rollbar.js'smergeskipsundefinedvalues, so the context stayed set after unmount.#88:
RollbarContextdoesn't match the docs. By default the context is set incomponentDidMount, and React only runs that after the children have rendered and mounted. So an error thrown while the children first render, which is exactly what the README'sErrorBoundarypairing is for, is reported with the previous context. The undocumentedonRenderprop sets the context during the first render instead. It also had two bugs:setStatefromrender, which triggers React's "Cannot update during an existing state transition" warningcontextpropNested contexts (from review). Making
onRenderfollow prop changes meant that changing an outer context overwrote an inner one that was still mounted, because React runs update lifecycles child-first. Nesting was already broken onmainwithoutonRenderand in the hook: children mount before parents, so the outer context won at mount, and unmounting restored in the wrong order, leaving the inner context set after everything unmounted.What changed
src/context-stack.js(new): the active contexts for each Rollbar client, shared by the component and the hook.previous ?? ''.rollbar.options.payload?.context, so a config withoutpayloadworks.src/rollbar-context.jsonRenderno longer callssetStateduring render.onRender, the entry is added during the first render, andcomponentDidUpdateapplies a changedcontextprop.onRender: added on mount, re-applied on update, removed on unmount, as before.src/hooks/use-rollbar-context.js: uses the same stack: adds its entry in an effect, updates it whenctxchanges, and removes it on unmount.README.mdRollbarContext"works for yourErrorBoundary".onRender. It explains whyonRenderisn't the default: it's a side effect inrender, and nothing restores the context if React throws that render away (for example an interrupted transition).useRollbarContexthas the same timing as the default.src/tests/components/rollbar-context.test.tsx: new tests. They're TSX and use Fix historyContext and RollbarContext typings, type-check index.d.ts in CI (#69) #162'sRollbarContexttypings (onRender), so ts-jest andnpm run typechecktype-check them.Why
''rather thanundefinedornullon restoreconfigure()ignoresundefined.nullwould be sent as the string"null", becausebuildPayloadinapiUtility.jsstringifies any non-string context. For an unset context,buildPayloadalready sends''(contextResult.value || ''), so restoring''produces the same item as never having set one. I checked themergeandbuildPayloadbehaviour in both rollbar 2.26.4 and 3.1.0.Not changed: making
onRenderthe defaultThat would fix the
ErrorBoundarycase for everyone. But it would move a global side effect intorenderfor every user, with the leak described above under concurrent rendering. I think that deserves its own decision rather than riding along with a bug fix.Validation
Node 20.19, React 17, rollbar 2.26.4 (the repo's dev dependency):
npm run typecheck:11 files, no errorsnpx jest: 25/25 passing (4 suites, 12 new tests)main'ssrc/rollbar-context.jsandsrc/hooks/use-rollbar-context.js, 8 of the new tests fail:onRender, the hook, and the hook inside the componentrootby default and withhomeunderonRender.npm run lint -- --max-warnings 0: passnpm run lint:examplesandnpm run test:examples: pass, with the examples using this branch's buildnpm run build: pass.?.and??are transpiled indist/andlib/.🤖 Generated with Claude Code