Skip to content

fix: attach componentStack as string so ErrorBoundary works on React Native#21

Merged
bobbyg603 merged 10 commits intomainfrom
fix/componentstack-react-native
Apr 18, 2026
Merged

fix: attach componentStack as string so ErrorBoundary works on React Native#21
bobbyg603 merged 10 commits intomainfrom
fix/componentstack-react-native

Conversation

@bobbyg603
Copy link
Copy Markdown
Member

@bobbyg603 bobbyg603 commented Apr 17, 2026

Summary

  • Fixes TypeError: Network request failed when <ErrorBoundary> catches a render error in a React Native app (@bugsplat/expo, bare RN). React Native's FormData polyfill can't serialize browser Blob objects, so the old `new Blob([componentStack])` attachment caused the multipart body to never assemble and `fetch` rejected before the request left the device.
  • Builds the componentStack attachment in whatever shape the runtime's FormData expects:
    • Browser: `new Blob([text], { type: 'text/plain' })` → real multipart file part (`Content-Disposition: filename=...`).
    • React Native: `{ uri: 'data:text/plain;base64,...', type: 'text/plain' }` using RN's native file-ref shape. RN's fetch decodes the data URI and uploads it as a file.
  • The bugsplat SDK stays uniform — it just serializes the `BugSplatAttachment` it receives. This is the only library that has to care about its runtime, so the single `isReactNative` check lives here.

Dependency

Requires bugsplat-js#78 for the `BugSplatFileRef` shape in the `BugSplatAttachment.data` union. I'll bump `"bugsplat": "^9.1.0"` after that SDK publishes.

Test plan

  • `npm test` — 19 tests pass, including a new assertion that the attachment is a `text/plain` Blob in the JSDOM (browser) test environment
  • `npm run build` — clean
  • End-to-end verified in `@bugsplat/expo`'s example app on an Android emulator: tapping "Trigger Render Error" posts to BugSplat and the `componentStack.txt` file attachment shows up in the dashboard. Confirmed over multiple taps.

🤖 Generated with Claude Code

…Native

ErrorBoundary was wrapping the componentStack in `new Blob([...])` before
handing it to the bugsplat SDK's FormData body. React Native's FormData
polyfill can't serialize browser Blob objects, so the multipart body
never assembles and fetch throws `TypeError: Network request failed` on
every render error caught on iOS and Android.

Passing the componentStack as a plain string works in all environments
because `FormData.append(name, string)` is universally supported. Requires
bugsplat@>=9.1 to accept string in `BugSplatAttachment.data`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 17, 2026 21:01
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates ErrorBoundary’s component stack attachment format to be React Native compatible by sending the componentStack as a plain string (instead of a browser Blob), avoiding RN FormData serialization failures during crash posting.

Changes:

  • Update createComponentStackAttachment to set data to the raw componentStack string.
  • Expand the inline doc comment to explain the React Native FormData/Blob incompatibility.
  • Add a unit test asserting the attachment data is a string containing the component name.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/ErrorBoundary.tsx Switch component stack attachment payload from Blob to string and document the RN rationale.
spec/ErrorBoundary.spec.tsx Add test coverage to ensure ErrorBoundary posts a string attachment payload for the component stack.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Browser FormData needs a real Blob to emit a multipart file part (with
a filename in Content-Disposition). React Native's FormData can't
serialize browser Blobs, but it can stream a file from a `data:` URI
when given RN's `{ uri, type }` shape.

Build the right shape once here based on navigator.product, so the
underlying bugsplat SDK never has to know which runtime it's in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bobbyg603 and others added 6 commits April 17, 2026 18:35
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.1 adds the BugSplatFileRef shape to the BugSplatAttachment.data
union, which this branch relies on when building the componentStack
attachment for React Native (data URI inside {uri, type}). Without
the bump, CI's npm ci pulled 9.0.x and tsc failed with
`'uri' does not exist in type 'Blob | Uint8Array'`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ErrorBoundary's componentStack attachment was built with a runtime
isReactNative check that picked between `new Blob` on web and a data-URI
file-ref on RN. Platform detection doesn't belong in a cross-platform
React helper, so this extends Scope (the existing DI container
ErrorBoundary already reads from) with a pluggable function slot
`createComponentStackAttachment`.

- Scope stores the builder plus a web-friendly default that wraps the
  stack in a text/plain Blob.
- ErrorBoundary reads the builder from scope via the existing optional
  `getCreateComponentStackAttachment()` getter; unknown/bare duck-typed
  scopes fall through to the default.
- appScope is now exported so runtime-specific wrappers (notably
  @bugsplat/expo) can call setCreateComponentStackAttachment() in their
  own init() without constructing and threading a separate Scope.
- Skip the attachment entirely if React hands us a null componentStack.

No behavior change for existing web consumers — the default builder
produces the same Blob they were getting before. RN consumers install
their own builder via the new seam.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scope's constructor always installs a default builder, so every
`new Scope()` exposes a valid `getCreateComponentStackAttachment()`.
The optional chain + fallback in ErrorBoundary only existed to support
bare duck-typed scope objects in tests — a narrow case not worth the
branch.

Tighten the scope prop to `Pick<Scope, 'getClient' | 'getCreateComponentStackAttachment'>`
and call the getter directly. Tests switch from ad-hoc `{ getClient }`
objects to real `new Scope(client)` instances, which is a closer
simulation of production usage anyway. ErrorBoundary's default
`scope` prop now points at the exported `appScope` directly instead
of wrapping `getBugSplat` in a one-off object literal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 9 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/scope.ts
Comment thread src/scope.ts
Comment thread src/ErrorBoundary.tsx
Comment thread src/ErrorBoundary.tsx
Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/cd.yml
CI and CD were bumped to Node 24 in 60ce396 but .nvmrc was left at 18,
which causes "works locally, fails in CI" drift for anyone who uses
`nvm use` on this repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bobbyg603 bobbyg603 merged commit 67e3c84 into main Apr 18, 2026
4 checks passed
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.

3 participants