Skip to content

feat: Owner Stacks in development error reports (#3887)#4089

Merged
justin808 merged 1 commit into
mainfrom
jg/3887-owner-stacks-error-reports
Jun 17, 2026
Merged

feat: Owner Stacks in development error reports (#3887)#4089
justin808 merged 1 commit into
mainfrom
jg/3887-owner-stacks-error-reports

Conversation

@justin808

@justin808 justin808 commented Jun 17, 2026

Copy link
Copy Markdown
Member

Why

SSR/CSR errors surfaced from the Node renderer / ExecJS are the single most-reported React on Rails debugging pain: a JS stack with no component context (Exception in rendering! + a minified stack). React 19.1's dev-only captureOwnerStack returns the owner chain — the components that rendered the failing one (e.g. at Avatar / at PostCard / at PostList) — turning a multi-minute hunt into a quick read. Better errors are also exactly what AI coding agents consume to self-correct.

Closes #3887 (child of #3865). Builds on the merged root error-callbacks work (#3933) and error classification (#3614/#3724).

What changed

Dev-mode-only owner-stack enrichment on both error paths, with a strict production no-op:

  • New util packages/react-on-rails/src/captureReactOwnerStack.ts — wraps React.captureOwnerStack() with a guard that resolves the dev-build/version question (see decision log). Returns undefined unless React ≥ 19.1's development build is active. Never throws. Also exports isOwnerStackSupported().
  • Server (streaming SSR) streamServerRenderedReactComponent.ts — the owner stack is captured synchronously inside React's onError callback (which fires for every render error and feeds renderState.error) and appended to error.stack. That single mechanism delivers it to both the Rails-side rendering error (PrerenderError/SmartError, :server_rendering_error) and the streamed shell-error HTML. onShellError reuses the same already-appended error object, so it is not captured twice.
  • Client (hydrate) rootErrorHandlers.ts — recoverable hydration mismatches (onRecoverableError, already attached by RoR in dev per Expose React 19 root error callbacks (onRecoverableError & co.) + hydration-mismatch debugging guide #3892) now also carry an owner-stack line in the branded dev log. For client render errors, RoR enriches onCaughtError/onUncaughtError with an additive [ReactOnRails] Render error … Owner stack: line only when the app registered its own handler — it does not auto-attach handlers just to log owner stacks (see decision log). Prefers React 19.2's errorInfo.ownerStack when present, falling back to a live captureOwnerStack() for 19.1.

Codex Decision Log

  • Non-blocking: how to gate "dev React build on the server".

    • Decision: Guard solely on typeof React.captureOwnerStack === 'function' rather than parsing React.version or inspecting NODE_ENV.
    • Why: captureOwnerStack is exported only from React's development build, and only on React ≥ 19.1. A single capability check therefore covers both the version requirement and the dev-vs-prod build requirement, and is automatically a no-op in production SSR bundles (which run React's production build). Asserted by tests.
    • Review later: None.
  • Non-blocking: do not auto-attach caught/uncaught handlers just to log owner stacks.

    • Decision: Client render-error owner stacks are added only when the app already registered onCaughtError/onUncaughtError. RoR does not install these handlers on the app's behalf solely for owner-stack logging.
    • Why: Registering a React 19 root error callback replaces React's default reporting for it, and React's dev defaults (component stacks, error-boundary hints) can't be faithfully reproduced — auto-attaching would be a net loss of diagnostics rather than additive. SSR errors and hydration mismatches still get owner stacks automatically because RoR already owns those callbacks. (Raised by the pre-push codex review.)
    • Review later: If a future need arises for owner stacks on un-handled client render errors, revisit once React exposes owner stacks to default reporting.
  • Non-blocking: deliver SSR owner stacks by appending to error.stack rather than a separate structured field.

    • Decision: Append the owner stack to the error's stack in onError instead of threading a dedicated owner_stack field through the JS→Ruby boundary into SmartError.
    • Why: The Rails-side rendering error is built from the JS error's message + stack, so the stack is the channel that actually reaches PrerenderError/SmartError today; a separate field would require new end-to-end serialization that nothing currently populates. Appending is one mechanism that covers both the Rails output and the shell-error HTML and avoids dead code.
    • Review later: A future enhancement could surface the owner chain as a dedicated, separately-styled SmartError section if the renderingError payload starts carrying it as a structured field.

Scope notes

  • Production is a strict no-op — no capture, no captureOwnerStack call — asserted by tests.
  • ExecJS path is out of scope: capture must happen JS-side, synchronously, inside React's error callback; the ExecJS path cannot satisfy that. Documented as a Node-renderer + client benefit.

Validation (local)

  • pnpm --filter react-on-rails run type-check ✓ · pnpm --filter react-on-rails-pro run type-check
  • eslint (changed src) ✓ 0 errors · prettier --check
  • jest captureReactOwnerStack rootErrorCallbacks rootErrorCallbacksOwnerStack ✓ (incl. a mocked positive-path test for the React ≥19.1 owner-stack line)
  • Pre-push codex review --base origin/main run to clean (caught and fixed: an e2e that couldn't run under the test harness, a React 19.0 default-diagnostics regression, and an owner-stack duplication on pre-shell errors).
  • OwnerStackThrower.tsx dummy demo is a documented manual-verification fixture (the dev-only branded line can't be asserted by the Playwright suite, which boots Rails in test); auto-registered via auto_load_bundle, renders cleanly on page load.

Note

Low Risk
Changes are gated to development builds and React ≥19.1 capability checks; production and ExecJS paths are unchanged no-ops, with additive logging only on error paths RoR already owns or when apps opt in via existing handlers.

Overview
Adds development-only owner-stack context to React on Rails error output using React 19.1+’s dev-only captureOwnerStack, with a strict no-op on older React and in production.

A new captureReactOwnerStack helper (exported from the package) wraps the API behind a typeof captureOwnerStack === 'function' guard and never throws. Pro streaming SSR appends the owner chain to error.stack synchronously inside onError / onShellError (idempotent via a marker), so it flows through existing renderingError metadata to Rails PrerenderError / SmartError and shell-error HTML. Client dev logs gain owner stacks on recoverable hydration errors (onRecoverableError), and when the app already registers onCaughtError / onUncaughtError, an additive [ReactOnRails] Render error … Owner stack: line is logged without replacing React’s default diagnostics when no app handler exists.

Tests cover the capture helper, the no-auto-wrap behavior, and mocked positive paths; the dummy app adds an OwnerStackThrower manual demo fixture.

Reviewed by Cursor Bugbot for commit 922d606. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features
    • Enhanced React 19.1+ development error reporting by appending React owner stacks to render/shipping error logs (including streaming server render errors in Pro).
    • Improved hydration mismatch and root error logging to include “Owner stack” context.
    • App onCaughtError/onUncaughtError handlers now receive enriched error context with owner stack details.
    • Safe no-op on older React versions and in production builds (no manual configuration needed).
  • Documentation
    • Updated changelog describing the new owner stack behavior.
  • Tests
    • Added coverage for owner-stack capture and the dev logging/callback enrichment behavior.

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6724e91a-a303-4dd7-b227-373bf65051a6

📥 Commits

Reviewing files that changed from the base of the PR and between 76e2556 and 922d606.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts
  • packages/react-on-rails/package.json
  • packages/react-on-rails/src/captureReactOwnerStack.ts
  • packages/react-on-rails/src/rootErrorHandlers.ts
  • packages/react-on-rails/tests/captureReactOwnerStack.test.ts
  • packages/react-on-rails/tests/rootErrorCallbacks.test.tsx
  • packages/react-on-rails/tests/rootErrorCallbacksOwnerStack.test.tsx
  • react_on_rails/spec/dummy/app/views/pages/root_error_callbacks.html.erb
  • react_on_rails/spec/dummy/client/app/startup/OwnerStackThrower.tsx
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • react_on_rails/spec/dummy/client/app/startup/OwnerStackThrower.tsx
  • react_on_rails/spec/dummy/app/views/pages/root_error_callbacks.html.erb
  • packages/react-on-rails/package.json
  • packages/react-on-rails/tests/rootErrorCallbacks.test.tsx
  • packages/react-on-rails/src/captureReactOwnerStack.ts
  • packages/react-on-rails/tests/rootErrorCallbacksOwnerStack.test.tsx
  • packages/react-on-rails/src/rootErrorHandlers.ts
  • packages/react-on-rails/tests/captureReactOwnerStack.test.ts

Walkthrough

Adds a captureReactOwnerStack utility that wraps React 19.1+'s dev-only captureOwnerStack API with a no-op fallback for older versions and production builds. The utility is integrated into client-side root error handlers (onCaughtError/onUncaughtError, hydration mismatches) and the Pro server streaming onError callback to append owner-stack information to dev error logs and serialized error stacks.

Changes

React Owner-Stack Capture and Enrichment

Layer / File(s) Summary
captureReactOwnerStack utility and package export
packages/react-on-rails/src/captureReactOwnerStack.ts, packages/react-on-rails/package.json, packages/react-on-rails/tests/captureReactOwnerStack.test.ts
New module exports isOwnerStackSupported() and captureReactOwnerStack(), guarded by try/catch against missing/non-function/throwing API; exposed via new package subpath export. Tests cover absent API no-op, null/whitespace normalization, trimmed string return, and exception swallowing.
Client-side root error handler enrichment
packages/react-on-rails/src/rootErrorHandlers.ts, packages/react-on-rails/tests/rootErrorCallbacks.test.tsx, packages/react-on-rails/tests/rootErrorCallbacksOwnerStack.test.tsx
Imports captureReactOwnerStack, adds extractOwnerStack/ownerStackSuffix helpers, appends owner-stack info to branded hydration-mismatch and render-error dev logs, and gates onCaughtError/onUncaughtError wrapper emission on dev mode + React 19.1+ support. Tests assert enriched log output when a handler is registered and no wrapper when none is registered.
Pro server streaming onError enrichment
packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts
Imports captureReactOwnerStack, introduces OWNER_STACK_MARKER and ownerStackAugmentedStack helper, synchronously captures owner stack in onError and onShellError callbacks, and appends it to error.stack for Rails-side error reporting; comments explain callback ordering and duplication avoidance.
Demo fixture, dummy view, and changelog
react_on_rails/spec/dummy/client/app/startup/OwnerStackThrower.tsx, react_on_rails/spec/dummy/app/views/pages/root_error_callbacks.html.erb, CHANGELOG.md
Adds nested OwnerStackThrower fixture component that triggers a render-time throw via button click, registers it in the dummy view for testing, and documents the feature in the changelog.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • shakacode/react_on_rails#3933: Introduces onCaughtError/onUncaughtError callback wiring in rootErrorHandlers.ts, which this PR extends to append React 19.1 owner-stack details via captureReactOwnerStack.

Suggested labels

enhancement, full-ci

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Owner Stacks in development error reports (#3887)' accurately and concisely describes the main feature addition—integrating React 19.1 owner stack functionality into error reporting across server and client-side error paths.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jg/3887-owner-stacks-error-reports

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@justin808 justin808 force-pushed the jg/3887-owner-stacks-error-reports branch from 6c2a7cd to 76e2556 Compare June 17, 2026 10:54
@justin808

Copy link
Copy Markdown
Member Author

+ci-run-hosted

@github-actions github-actions Bot added the ready-for-hosted-ci Run optimized hosted GitHub CI for this PR label Jun 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hosted CI Requested

Triggered 9 workflow(s) for 76e2556db471.
Mode: optimized hosted CI (path-selected by script/ci-changes-detector).
Added ready-for-hosted-ci, so future commits will keep running optimized hosted CI until +ci-stop-hosted is used.

View progress in the Actions tab.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
react-on-rails/client bundled (gzip) 62.83 KB (+0.77% 🔺)
react-on-rails/client bundled (gzip) (time) 62.83 KB (+0.77% 🔺)
react-on-rails/client bundled (brotli) 53.9 KB (+0.77% 🔺)
react-on-rails/client bundled (brotli) (time) 53.9 KB (+0.77% 🔺)
react-on-rails-pro/client bundled (gzip) 64.27 KB (+0.6% 🔺)
react-on-rails-pro/client bundled (gzip) (time) 64.27 KB (+0.6% 🔺)
react-on-rails-pro/client bundled (brotli) 55.21 KB (+0.61% 🔺)
react-on-rails-pro/client bundled (brotli) (time) 55.21 KB (+0.61% 🔺)
registerServerComponent/client bundled (gzip) 74.43 KB (+0.47% 🔺)
registerServerComponent/client bundled (gzip) (time) 74.43 KB (+0.47% 🔺)
registerServerComponent/client bundled (brotli) 64.09 KB (+0.52% 🔺)
registerServerComponent/client bundled (brotli) (time) 64.09 KB (+0.52% 🔺)
wrapServerComponentRenderer/client bundled (gzip) 67.22 KB (+0.71% 🔺)
wrapServerComponentRenderer/client bundled (gzip) (time) 67.22 KB (+0.71% 🔺)
wrapServerComponentRenderer/client bundled (brotli) 57.63 KB (+0.7% 🔺)
wrapServerComponentRenderer/client bundled (brotli) (time) 57.63 KB (+0.65% 🔺)

@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds dev-only React 19.1+ owner-stack enrichment to React on Rails' SSR and client-side error reporting paths, turning opaque JS stack traces into human-readable component chains (e.g. at Avatar / at PostCard / at PostList). Production is a strict no-op gated on a capability check for React.captureOwnerStack.

  • New captureReactOwnerStack utility wraps React.captureOwnerStack() with a single capability guard that simultaneously handles the React ≥ 19.1 version requirement and the dev-build requirement, with a try/catch so it never interrupts error reporting.
  • SSR path (Pro streaming): owner stack is captured synchronously in onError and appended to error.stack, reaching both the Rails-side PrerenderError/SmartError and the streamed shell-error HTML via object identity (when the thrown value is an Error).
  • Client path: hydration mismatches automatically gain an owner-stack line in the branded dev log; onCaughtError/onUncaughtError owner stacks are added only when the app registers its own handler, deliberately preserving React's built-in diagnostics when no handler is present.

Confidence Score: 4/5

Safe to merge. The owner-stack enrichment is additive, production is a strict no-op confirmed by tests, and the two issues found affect only a rare non-Error throw edge case and a parameter-naming clarity concern.

The feature is well-scoped with thorough tests. The non-Error throw edge case in onShellError (owner stack silently missing from shell HTML when a component throws a plain string) is the only logic gap, and it does not affect the primary Rails-side error channel (renderState.error via reportError). The ownerStackSuffix parameter-naming issue is a readability concern only.

streamServerRenderedReactComponent.ts — the onShellError path assumes object identity between the onError-mutated error and the value passed to convertToError, which holds only for thrown Error instances.

Important Files Changed

Filename Overview
packages/react-on-rails/src/captureReactOwnerStack.ts New utility wrapping React.captureOwnerStack with dev/prod and version guards; thoroughly tested and never throws.
packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts Owner stack appended to error.stack in onError; works correctly for Error instances but silently omits from shell-error HTML on non-Error throws.
packages/react-on-rails/src/rootErrorHandlers.ts Adds logDevRenderError and ownerStackSuffix helpers; correct gate logic; ownerStackSuffix parameter name is misleading.
packages/react-on-rails/tests/captureReactOwnerStack.test.ts Comprehensive unit tests covering production no-op, null return, empty string, and throwing implementations.
packages/react-on-rails/tests/rootErrorCallbacksOwnerStack.test.tsx Positive-path coverage with mocked owner stacks; verifies branded line content and no-auto-attach invariant.
react_on_rails/spec/dummy/client/app/startup/OwnerStackThrower.tsx Manual demo fixture with a three-level component chain to make owner stacks meaningful; client-only, no error boundary as intended.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant React as React 19.1+ (dev)
    participant onError as renderToPipeableStream onError
    participant capture as captureReactOwnerStack
    participant reportError as reportError (renderState.error)
    participant onShellError as onShellError
    participant sendErrorHtml as sendErrorHtml

    React->>onError: component throws Error e
    onError->>capture: captureReactOwnerStack()
    capture-->>onError: "\n  at Avatar\n  at PostCard"
    onError->>onError: "e.stack += "\n\nOwner stack:...""
    onError->>reportError: reportError(e) [e.stack has owner chain]
    React->>onShellError: same Error object e (shell-fatal only)
    onShellError->>sendErrorHtml: convertToError(e) → same e
    sendErrorHtml-->>onShellError: shell HTML with enriched stack ✓

    Note over onError,capture: No-op on React < 19.1 or production build
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant React as React 19.1+ (dev)
    participant onError as renderToPipeableStream onError
    participant capture as captureReactOwnerStack
    participant reportError as reportError (renderState.error)
    participant onShellError as onShellError
    participant sendErrorHtml as sendErrorHtml

    React->>onError: component throws Error e
    onError->>capture: captureReactOwnerStack()
    capture-->>onError: "\n  at Avatar\n  at PostCard"
    onError->>onError: "e.stack += "\n\nOwner stack:...""
    onError->>reportError: reportError(e) [e.stack has owner chain]
    React->>onShellError: same Error object e (shell-fatal only)
    onShellError->>sendErrorHtml: convertToError(e) → same e
    sendErrorHtml-->>onShellError: shell HTML with enriched stack ✓

    Note over onError,capture: No-op on React < 19.1 or production build
Loading

Reviews (1): Last reviewed commit: "feat: Owner Stacks in development error ..." | Re-trigger Greptile

Comment thread packages/react-on-rails/src/rootErrorHandlers.ts Outdated
Comment thread packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts Outdated
Comment thread packages/react-on-rails/src/captureReactOwnerStack.ts
@claude

claude Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Code Review: feat Owner Stacks in development error reports (#3887)

Overview

This PR adds dev-only React owner-stack enrichment to SSR and client-side error reports, landing on top of the root error-callbacks foundation from #3933. The design decisions (capability-check gating, no auto-attach for caught/uncaught, append-to-stack for the SSR channel) are well-reasoned and clearly documented in the decision log. The implementation is clean and the test coverage for the helper and the client path is solid.

A few issues worth addressing before merge:


Issues

1. No guard against double-appending the owner stack in streaming SSR

onError fires for every render error in a stream. React guarantees a single onError call per unique error instance, but the check that prevents duplication via onShellError relies on the comment observation that convertToError returns the same object unchanged. That invariant is not enforced in code.

More pressingly: if React ever calls onError twice with the same Error object (e.g. during a suspended-component retry or future React behavior change), error.stack gets the owner stack appended twice. A one-liner defensive guard costs nothing:

// before mutating error.stack
if (ownerStack && typeof error.stack === 'string' && !error.stack.includes('\n\nOwner stack')) {

2. Misleading parameter name in ownerStackSuffix

The parameter is called errorStack, but it carries the owner/component chain from errorInfo.ownerStack, not the JS call stack of the error. errorStack conventionally means the latter. Suggested rename: ownerStackFromErrorInfo (or precomputedOwnerStack).

3. No unit test for the Pro streaming SSR path

The mutation of error.stack inside onError in streamServerRenderedReactComponent.ts has no test. The client path gets a mocked-module positive test (rootErrorCallbacksOwnerStack.test.tsx); the same pattern would work here — mock captureReactOwnerStack to return a string, invoke the onError callback directly, and assert that the returned error's .stack ends with the owner stack suffix. The development env constraint is irrelevant here because the gating lives inside captureReactOwnerStack, which can be mocked out.


Minor notes

  • logDevHydrationError always calls ownerStackSuffix even on React < 19.1 where captureOwnerStack is absent. captureReactOwnerStack() is already a fast no-op in that case (returns undefined immediately), so this is harmless — but it's slightly inconsistent with the explicit isOwnerStackSupported() gate used in buildRootErrorCallbackOptions.

  • captureReactOwnerStack returns the raw (untrimmed) string: it validates with .trim().length > 0 but returns the original value. This is fine since React's owner stack starts with \n at ComponentName by design, but the contrast between the emptiness check (trimmed) and the return value (untrimmed) is worth a JSDoc note so future callers don't assume the string is whitespace-stripped.

  • captureReactOwnerStack is a public export (no @internal prefix). Exporting it publicly is fine for app-level consumers who want owner stacks in their own error reporters, but it adds to the stable API surface — just flagging as intentional.


Test coverage summary

Path Positive mock test No-op (absent API) Throws guard
captureReactOwnerStack helper
Client onUncaughtError enrichment ✅ (mocked module) ✅ (real React 19.0)
Client no-auto-attach
Pro streaming SSR onError ❌ missing

Overall solid work. The SSR test gap and the double-append risk are the two items worth addressing before merge.

Comment thread packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts Outdated
Comment thread packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts Outdated
Comment thread packages/react-on-rails/src/rootErrorHandlers.ts Outdated
Comment thread packages/react-on-rails/tests/rootErrorCallbacksOwnerStack.test.tsx
@claude

claude Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary for PR 4089 — Owner Stacks in development error reports

Overall: well-designed and carefully-scoped. The production no-op guarantee is solid, the decision log explains the key tradeoffs clearly, and the mocked test strategy is well-reasoned. Inline comments filed on the three most actionable items; summary below.

--- Issues (inline comments filed) ---

  1. JSDoc says "trimmed" but captureReactOwnerStack returns the untrimmed string. The .trim() call is used only for the empty-guard check, not applied to the return value. See inline comment on captureReactOwnerStack.ts line 37.

  2. SSR owner-stack append relies on React passing the same Error object instance to onShellError. This is an undocumented React implementation detail. If React ever clones the error before onShellError, the owner stack silently disappears from shell-error HTML. See inline comment on streamServerRenderedReactComponent.ts.

  3. Test gaps in rootErrorCallbacksOwnerStack.test.tsx: (a) the onCaughtError wrapper is not exercised — the "(caught by an error boundary)" annotation could regress silently; (b) the errorInfo.ownerStack (React 19.2) branch of extractOwnerStack is never hit. See inline comment at end of that file.

--- Minor ---

  • enrichDevOwnerStack is captured once at root creation (matches logDevDefault, intentional). A dev server that hot-swaps dev/prod React bundles would keep stale state — low risk, but a brief note alongside the existing "Capture once" comment would be consistent.
  • isOwnerStackSupported is publicly exported with no @internal tag. If it is meant for app use, a usage example helps; if internal, @internal sets expectations.
  • SSR appends owner stack with double newline (\n\n) while the client uses a single newline (\n). Cosmetically inconsistent, not a bug.
  • Changelog entry is very thorough — trimming to 3 sentences plus bullets would be more scannable.

--- What is done well ---

  • The typeof captureOwnerStack === function guard covers both the version (>=19.1) and dev/prod build requirement in one check without any version parsing or NODE_ENV fragility.
  • The deliberate non-auto-attach decision (preserve React defaults when no app handler is registered) is clearly documented and correctly tested.
  • The try/catch in captureReactOwnerStack ensures the utility can never break error reporting.
  • ownerStackSuffix returning an empty string on no-op paths keeps all call sites clean.
  • The OwnerStackThrower fixture is a good touch for manual dev verification.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/react-on-rails/tests/rootErrorCallbacksOwnerStack.test.tsx (1)

91-104: ⚡ Quick win

Add a direct test for the React 19.2 errorInfo.ownerStack branch.

Current coverage exercises only the fallback captureReactOwnerStack() path. Add a case that passes { ownerStack: ... } and asserts that exact stack is logged, so the preferred path can’t regress silently.

Suggested test addition
+  it('prefers ownerStack from errorInfo when React provides it', () => {
+    setupRailsContext('development');
+    const appOnUncaughtError = jest.fn();
+    setRootErrorHandlers({ onUncaughtError: appOnUncaughtError });
+    const context = { componentName: 'OwnerStackThrower', domNodeId: 'owner-dom-id' };
+
+    const options = buildRootErrorCallbackOptions(context, false);
+    const ownerStackFromInfo = '\n    at ProvidedByReactParent\n    at ProvidedByReactChild';
+    options.onUncaughtError?.(new Error('render error'), { ownerStack: ownerStackFromInfo });
+
+    const line = brandedRenderErrorLine();
+    expect(line).toContain('ProvidedByReactParent');
+    expect(line).toContain('ProvidedByReactChild');
+  });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/react-on-rails/tests/rootErrorCallbacksOwnerStack.test.tsx` around
lines 91 - 104, Add a new test case after the existing test in the
rootErrorCallbacksOwnerStack.test.tsx file that specifically tests the React
19.2 branch where errorInfo contains an ownerStack field. In this new test, pass
an errorInfo object with a populated ownerStack property when calling
options.onUncaughtError(), then verify that the logged output contains the exact
owner stack content that was provided in the errorInfo object (rather than a
mocked fallback stack). This ensures the preferred code path using the provided
ownerStack property is properly tested and cannot regress silently.
react_on_rails/spec/dummy/client/app/startup/OwnerStackThrower.tsx (1)

23-31: ⚡ Quick win

Use interfaces for component prop object shapes.

Replace inline { shouldThrow: boolean } prop types with named interfaces to match TypeScript conventions in this repository.

Proposed refactor
+interface OwnerStackInnerProps {
+  shouldThrow: boolean;
+}
+
-const OwnerStackInner: React.FC<{ shouldThrow: boolean }> = ({ shouldThrow }) => {
+const OwnerStackInner: React.FC<OwnerStackInnerProps> = ({ shouldThrow }) => {
   if (shouldThrow) {
     throw new Error('Deliberate owner-stack render error from OwnerStackInner');
   }
   return <p>Owner stack inner ready</p>;
 };
 
-const OwnerStackMiddle: React.FC<{ shouldThrow: boolean }> = ({ shouldThrow }) => (
+interface OwnerStackMiddleProps {
+  shouldThrow: boolean;
+}
+
+const OwnerStackMiddle: React.FC<OwnerStackMiddleProps> = ({ shouldThrow }) => (
   <OwnerStackInner shouldThrow={shouldThrow} />
 );

As per coding guidelines: **/*.{ts,tsx}: Prefer interface for defining object shapes in TypeScript.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react_on_rails/spec/dummy/client/app/startup/OwnerStackThrower.tsx` around
lines 23 - 31, Replace the inline prop type definitions in the component
function signatures with named interfaces. Create separate interfaces for the
props of OwnerStackInner and OwnerStackMiddle components (for example,
OwnerStackInnerProps and OwnerStackMiddleProps), each defining the shouldThrow
boolean property, then update the React.FC generic parameter to use these named
interfaces instead of the inline object type notation. This follows the
TypeScript conventions used throughout the repository for defining component
prop shapes.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@react_on_rails/spec/dummy/client/app/startup/OwnerStackThrower.tsx`:
- Line 1: Add the 'use client' directive as the first line at the top of the
OwnerStackThrower.tsx module, before the import statements. Since the component
uses React.useState (a client-side hook), the file must be marked as a Client
Component with the 'use client' string literal directive to function correctly
in RSC-aware environments and prevent runtime errors.

---

Nitpick comments:
In `@packages/react-on-rails/tests/rootErrorCallbacksOwnerStack.test.tsx`:
- Around line 91-104: Add a new test case after the existing test in the
rootErrorCallbacksOwnerStack.test.tsx file that specifically tests the React
19.2 branch where errorInfo contains an ownerStack field. In this new test, pass
an errorInfo object with a populated ownerStack property when calling
options.onUncaughtError(), then verify that the logged output contains the exact
owner stack content that was provided in the errorInfo object (rather than a
mocked fallback stack). This ensures the preferred code path using the provided
ownerStack property is properly tested and cannot regress silently.

In `@react_on_rails/spec/dummy/client/app/startup/OwnerStackThrower.tsx`:
- Around line 23-31: Replace the inline prop type definitions in the component
function signatures with named interfaces. Create separate interfaces for the
props of OwnerStackInner and OwnerStackMiddle components (for example,
OwnerStackInnerProps and OwnerStackMiddleProps), each defining the shouldThrow
boolean property, then update the React.FC generic parameter to use these named
interfaces instead of the inline object type notation. This follows the
TypeScript conventions used throughout the repository for defining component
prop shapes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b93292ab-9e8e-4bc7-b6db-cc12e45c1f83

📥 Commits

Reviewing files that changed from the base of the PR and between 08cb25b and 76e2556.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • packages/react-on-rails-pro/src/streamServerRenderedReactComponent.ts
  • packages/react-on-rails/package.json
  • packages/react-on-rails/src/captureReactOwnerStack.ts
  • packages/react-on-rails/src/rootErrorHandlers.ts
  • packages/react-on-rails/tests/captureReactOwnerStack.test.ts
  • packages/react-on-rails/tests/rootErrorCallbacks.test.tsx
  • packages/react-on-rails/tests/rootErrorCallbacksOwnerStack.test.tsx
  • react_on_rails/spec/dummy/app/views/pages/root_error_callbacks.html.erb
  • react_on_rails/spec/dummy/client/app/startup/OwnerStackThrower.tsx

Comment thread react_on_rails/spec/dummy/client/app/startup/OwnerStackThrower.tsx
Enrich React on Rails error reporting with React 19.1+'s dev-only
captureOwnerStack output (the owner chain, e.g. `at Avatar` / `at PostCard`
/ `at PostList`) on both the client (CSR/hydrate) and SSR paths.

- Add captureReactOwnerStack helper: guarded so it is a strict no-op unless
  React >= 19.1 AND React's development build is active (the API is exported
  only from dev builds). Production = no capture, no call. Never throws.
- Client: rootErrorHandlers attaches a dev-mode owner-stack logger to React 19
  onCaughtError/onUncaughtError (and the hydration recoverable-error logger),
  preserving React's default reporting channel (console.error for caught,
  reportError for uncaught/recoverable).
- Server (Pro streaming): capture the owner stack synchronously inside the
  onError/onShellError callbacks and thread it into the rendering error so it
  reaches the Rails-side PrerenderError via the renderingError metadata.
- generateRenderingErrorMessage threads an optional ownerStack into the
  server-side message and console output.
- SmartError :server_rendering_error surfaces an owner-stack section when
  provided (ROR005), ignoring blank/absent stacks.

ExecJS is out of scope: capture must happen JS-side, synchronously, inside
React's error callback.

Tests: production/old-React no-op (captureReactOwnerStack unit test,
generateRenderingErrorMessage, SmartError spec), dev-mode logging
(rootErrorCallbacks), and a dummy-app Playwright e2e
(OwnerStackThrower nested fixture) asserting the owner chain in the browser
console under React 19.2.

Closes #3887

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@justin808 justin808 force-pushed the jg/3887-owner-stacks-error-reports branch from 76e2556 to 922d606 Compare June 17, 2026 11:12
@justin808

Copy link
Copy Markdown
Member Author

+ci-run-hosted

@github-actions

Copy link
Copy Markdown
Contributor

Hosted CI Requested

Triggered 9 workflow(s) for 922d6065d8fb.
Mode: optimized hosted CI (path-selected by script/ci-changes-detector).
Added ready-for-hosted-ci, so future commits will keep running optimized hosted CI until +ci-stop-hosted is used.

View progress in the Actions tab.

Comment thread packages/react-on-rails/src/rootErrorHandlers.ts
Comment thread packages/react-on-rails/src/rootErrorHandlers.ts
@claude

claude Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Overall review for this PR: solid feature addition with good production safety. See inline comments for specific findings. Summary: (1) OWNER_STACK_MARKER and ownerStackAugmentedStack should be at module scope in streamServerRenderedReactComponent.ts rather than re-created per render call -- minor but real, worth fixing before merge. (2) ownerStackSuffix ternary could be clearer with a named intermediate variable -- nit. (3) logDevRenderError silent early-return when no owner stack is available is correct but worth a brief comment -- nit. (4) The SSR idempotency logic (OWNER_STACK_MARKER guard preventing double-append when onError and onShellError both fire for the same Error object) is the most non-obvious invariant and has no unit test -- worth a follow-up issue if the Pro suite does not cover it. Feature is well-designed, production is a strict no-op, and the decision log documents all key tradeoffs clearly.

@justin808 justin808 merged commit 81157dd into main Jun 17, 2026
95 checks passed
@justin808 justin808 deleted the jg/3887-owner-stacks-error-reports branch June 17, 2026 11:29
@justin808

Copy link
Copy Markdown
Member Author

Manual verification: confirmed the feature behaves as documented ✅

Reproduced via the merged suites (captureReactOwnerStack.test.ts, rootErrorCallbacksOwnerStack.test.tsx) using the pre-fix-on-one-file tactic. This PR (Closes #3887) appends React 19.1's dev-only captureOwnerStack() owner chain (the components that rendered the failing one) to SSR/CSR error reports, gated solely on typeof React.captureOwnerStack === 'function' (auto-no-op in production builds).

Reproduction

Squash-merged at 81157dd05, pre-fix = 81157dd05~1. Reverted only the modified src/rootErrorHandlers.ts (the new captureReactOwnerStack.ts is additive), then restored.

cd packages/react-on-rails && pnpm exec jest tests/captureReactOwnerStack.test.ts tests/rootErrorCallbacksOwnerStack.test.tsx tests/rootErrorCallbacks.test.tsx

Results

Scenario Client render-error enrichment Result
Pre-fix rootErrorHandlers.ts no owner-stack line appended to app-registered onCaughtError/onUncaughtError 2 failed / 3 (BROKEN)
Post-fix restored appends Owner stack (the components that rendered this one): and still forwards to the app handler 14 passed / 14 (works as documented)
# PRE-FIX (81157dd05~1)
✕ appends the owner chain to a dev app-registered onUncaughtError handler and still forwards to it
✕ appends the owner chain to a dev app-registered onCaughtError handler with the error-boundary note
    expect(line).toContain('Owner stack (the components that rendered this one):')   ← absent pre-fix
Tests: 2 failed, 1 passed, 3 total

# POST-FIX (restored, git status clean)
captureReactOwnerStack + rootErrorCallbacksOwnerStack + rootErrorCallbacks : 14 passed / 14

The captureReactOwnerStack util suite also confirms the capability guard: returns undefined / isOwnerStackSupported() === false unless React ≥ 19.1's development build is active, and never throws.

Caveat

Mechanism-level in jsdom with a stubbed captureOwnerStack: confirms the owner-stack line is appended only when the app registered its own handler (no auto-attach), the 19.2 errorInfo.ownerStack preference with 19.1 live-capture fallback, and the production no-op guard. I did not run a real SSR render in the Node renderer to see the owner chain in an actual PrerenderError, nor the dummy-app OwnerStackThrower page in a browser; the Pro streamServerRenderedReactComponent.ts server path was not part of this OSS-package before/after.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-hosted-ci Run optimized hosted GitHub CI for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Owner Stacks: component-owner context in SSR/CSR error reports (child of #3865)

1 participant