From 6f698760fd6899e60240ab3d002f946182ef70f6 Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Fri, 17 Apr 2026 17:00:55 -0400 Subject: [PATCH 01/10] fix: attach componentStack as string so ErrorBoundary works on React 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) --- spec/ErrorBoundary.spec.tsx | 17 +++++++++++++++++ src/ErrorBoundary.tsx | 9 +++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/spec/ErrorBoundary.spec.tsx b/spec/ErrorBoundary.spec.tsx index ff9573d..dc07000 100644 --- a/spec/ErrorBoundary.spec.tsx +++ b/spec/ErrorBoundary.spec.tsx @@ -152,6 +152,23 @@ describe('', () => { await waitFor(() => expect(mockPost).toHaveBeenCalledTimes(1)); }); + it('should attach componentStack as a plain string (React Native friendly)', async () => { + render( + + + + ); + + await waitFor(() => expect(mockPost).toHaveBeenCalledTimes(1)); + + const [, options] = mockPost.mock.calls[0]; + expect(options.attachments).toHaveLength(1); + const [attachment] = options.attachments; + expect(attachment.filename).toBe('componentStack.txt'); + expect(typeof attachment.data).toBe('string'); + expect(attachment.data).toContain('BlowUp'); + }); + it('should call beforePost', async () => { render( Date: Fri, 17 Apr 2026 17:20:01 -0400 Subject: [PATCH 02/10] fix: build componentStack attachment per platform 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) --- spec/ErrorBoundary.spec.tsx | 6 +++--- src/ErrorBoundary.tsx | 31 ++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/spec/ErrorBoundary.spec.tsx b/spec/ErrorBoundary.spec.tsx index dc07000..31b4385 100644 --- a/spec/ErrorBoundary.spec.tsx +++ b/spec/ErrorBoundary.spec.tsx @@ -152,7 +152,7 @@ describe('', () => { await waitFor(() => expect(mockPost).toHaveBeenCalledTimes(1)); }); - it('should attach componentStack as a plain string (React Native friendly)', async () => { + it('should attach componentStack as a text/plain Blob in browser environments', async () => { render( @@ -165,8 +165,8 @@ describe('', () => { expect(options.attachments).toHaveLength(1); const [attachment] = options.attachments; expect(attachment.filename).toBe('componentStack.txt'); - expect(typeof attachment.data).toBe('string'); - expect(attachment.data).toContain('BlowUp'); + expect(attachment.data).toBeInstanceOf(Blob); + expect(attachment.data.type).toBe('text/plain'); }); it('should call beforePost', async () => { diff --git a/src/ErrorBoundary.tsx b/src/ErrorBoundary.tsx index cbb1dc2..2315d45 100644 --- a/src/ErrorBoundary.tsx +++ b/src/ErrorBoundary.tsx @@ -28,20 +28,41 @@ function isArrayDiff(a: unknown[] = [], b: unknown[] = []) { return a.some((item, index) => !Object.is(item, b[index])); } +const isReactNative = + typeof navigator !== 'undefined' && + (navigator as { product?: string }).product === 'ReactNative'; + +function utf8ToBase64(text: string): string { + const NodeBuffer = (globalThis as { Buffer?: { from(s: string, enc: string): { toString(enc: string): string } } }).Buffer; + if (NodeBuffer) { + return NodeBuffer.from(text, 'utf-8').toString('base64'); + } + return btoa(unescape(encodeURIComponent(text))); +} + /** * Pack a component stack trace string into an attachment. * - * The stack is appended as a plain string (not wrapped in a `Blob`) so that - * React Native's FormData polyfill — which can't serialize browser `Blob` - * objects — can upload it successfully. Browsers accept strings on - * `FormData.append()` as well, so this works everywhere. + * On web we wrap the text in a `Blob` so FormData sends it as a real file + * part. On React Native we can't use a `Blob` (RN's FormData polyfill can't + * serialize browser Blobs), so we encode the text as a base64 `data:` URI + * inside RN's native `{ uri, type }` file shape instead. */ function createComponentStackAttachment( componentStack: string ): BugSplatAttachment { + if (isReactNative) { + return { + filename: 'componentStack.txt', + data: { + uri: `data:text/plain;base64,${utf8ToBase64(componentStack)}`, + type: 'text/plain', + }, + }; + } return { filename: 'componentStack.txt', - data: componentStack, + data: new Blob([componentStack], { type: 'text/plain' }), }; } From 60ce396afca3083b47ae8e40fc8e02fa7c5c93d3 Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Fri, 17 Apr 2026 18:31:14 -0400 Subject: [PATCH 03/10] ci: bump node to 24 and action versions to v4 Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/cd.yml | 6 +++--- .github/workflows/ci.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index af23d50..035062d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: lts/* + node-version: 24 cache: 'npm' - name: Install Dependencies diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5381dba..5575ff0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,13 +15,13 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18] + node-version: [24] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Node v${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' From d5f5631f8059fdeae648909582509d7a2821c4ff Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Fri, 17 Apr 2026 18:35:24 -0400 Subject: [PATCH 04/10] chore: npm audit fix Co-Authored-By: Claude Opus 4.7 (1M context) --- package-lock.json | 269 ++++++++++++++++++++++------------------------ 1 file changed, 131 insertions(+), 138 deletions(-) diff --git a/package-lock.json b/package-lock.json index e531b6d..e351501 100644 --- a/package-lock.json +++ b/package-lock.json @@ -807,21 +807,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", - "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", - "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "dev": true, "license": "MIT", "optional": true, @@ -830,9 +830,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, @@ -2050,20 +2050,10 @@ "node": ">= 8" } }, - "node_modules/@oxc-project/runtime": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", - "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@oxc-project/types": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", - "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", "dev": true, "license": "MIT", "funding": { @@ -2095,9 +2085,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", "cpu": [ "arm64" ], @@ -2112,9 +2102,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", "cpu": [ "arm64" ], @@ -2129,9 +2119,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", "cpu": [ "x64" ], @@ -2146,9 +2136,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", "cpu": [ "x64" ], @@ -2163,9 +2153,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", - "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", "cpu": [ "arm" ], @@ -2180,9 +2170,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", "cpu": [ "arm64" ], @@ -2197,9 +2187,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", "cpu": [ "arm64" ], @@ -2214,9 +2204,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", "cpu": [ "ppc64" ], @@ -2231,9 +2221,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", "cpu": [ "s390x" ], @@ -2248,9 +2238,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", "cpu": [ "x64" ], @@ -2265,9 +2255,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", "cpu": [ "x64" ], @@ -2282,9 +2272,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", "cpu": [ "arm64" ], @@ -2299,9 +2289,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", - "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", "cpu": [ "wasm32" ], @@ -2309,33 +2299,37 @@ "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", "cpu": [ "arm64" ], @@ -2350,9 +2344,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", "cpu": [ "x64" ], @@ -3503,9 +3497,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -4037,9 +4031,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -5936,9 +5930,9 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -6201,9 +6195,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -6308,9 +6302,9 @@ "license": "MIT" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8590,9 +8584,9 @@ } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -9259,9 +9253,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -9492,9 +9486,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -10223,9 +10217,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -10577,9 +10571,9 @@ } }, "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -10786,9 +10780,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10860,14 +10854,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", - "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.115.0", - "@rolldown/pluginutils": "1.0.0-rc.9" + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" @@ -10876,27 +10870,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.9", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", - "@rolldown/binding-darwin-x64": "1.0.0-rc.9", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.9", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", - "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", "dev": true, "license": "MIT" }, @@ -12291,17 +12285,16 @@ } }, "node_modules/vite": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", - "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", + "picomatch": "^4.0.4", "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.9", + "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "bin": { @@ -12318,8 +12311,8 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.0.0-alpha.31", - "esbuild": "^0.27.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", From d5f894a3e0e2a7a4bb90b53a7aabc886735a60fc Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Fri, 17 Apr 2026 19:26:54 -0400 Subject: [PATCH 05/10] chore: bump bugsplat dep to ^9.1.0 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) --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e351501..13dc108 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "examples/*" ], "dependencies": { - "bugsplat": "^9.0.0" + "bugsplat": "^9.1.0" }, "devDependencies": { "@bugsplat/js-api-client": "^2.0.0", @@ -4154,9 +4154,9 @@ "license": "MIT" }, "node_modules/bugsplat": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/bugsplat/-/bugsplat-9.0.1.tgz", - "integrity": "sha512-VjfukkURuvswE6SAQg+4APtqDEJliW+OOZ2OR1pUsAQnvUYe75eGpXFMqvU2bLE1abOc9GVSv+lI2LGrJv75RQ==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/bugsplat/-/bugsplat-9.1.0.tgz", + "integrity": "sha512-QIfY6kMtlMwJQz2Beg42QJ65TgkKP49jv5zBVCX2X86CDdCRFY0i/B9s1k3aqElJMRLVkiWxUut6FRTn2Xx5fQ==", "license": "MIT", "engines": { "node": ">=16.0.0", diff --git a/package.json b/package.json index 499ba36..7df8e4d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "author": "zmrl", "license": "MIT", "dependencies": { - "bugsplat": "^9.0.0" + "bugsplat": "^9.1.0" }, "devDependencies": { "@bugsplat/js-api-client": "^2.0.0", From 554812897e2a934093083b641d92b8cc3fb1d1c5 Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Fri, 17 Apr 2026 21:25:22 -0400 Subject: [PATCH 06/10] refactor: replace isReactNative with Scope-injected attachment builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- spec/ErrorBoundary.spec.tsx | 23 +++++++++++++- src/ErrorBoundary.tsx | 60 +++++++++++-------------------------- src/appScope.ts | 9 ++++-- src/index.ts | 1 + src/scope.ts | 47 +++++++++++++++++++++++++++-- 5 files changed, 92 insertions(+), 48 deletions(-) diff --git a/spec/ErrorBoundary.spec.tsx b/spec/ErrorBoundary.spec.tsx index 31b4385..3bd38cc 100644 --- a/spec/ErrorBoundary.spec.tsx +++ b/spec/ErrorBoundary.spec.tsx @@ -152,7 +152,7 @@ describe('', () => { await waitFor(() => expect(mockPost).toHaveBeenCalledTimes(1)); }); - it('should attach componentStack as a text/plain Blob in browser environments', async () => { + it('should attach componentStack as a text/plain Blob by default', async () => { render( @@ -169,6 +169,27 @@ describe('', () => { expect(attachment.data.type).toBe('text/plain'); }); + it('honors scope.getCreateComponentStackAttachment() when provided', async () => { + const customAttachment = { filename: 'componentStack.txt', data: 'CUSTOM' }; + const customBuilder = jest.fn(() => customAttachment); + const scopeWithBuilder = { + getClient: () => bugSplat, + getCreateComponentStackAttachment: () => customBuilder, + }; + + render( + + + + ); + + await waitFor(() => expect(mockPost).toHaveBeenCalledTimes(1)); + + expect(customBuilder).toHaveBeenCalledWith(expect.stringContaining('BlowUp')); + const [, options] = mockPost.mock.calls[0]; + expect(options.attachments).toEqual([customAttachment]); + }); + it('should call beforePost', async () => { render( !Object.is(item, b[index])); } -const isReactNative = - typeof navigator !== 'undefined' && - (navigator as { product?: string }).product === 'ReactNative'; - -function utf8ToBase64(text: string): string { - const NodeBuffer = (globalThis as { Buffer?: { from(s: string, enc: string): { toString(enc: string): string } } }).Buffer; - if (NodeBuffer) { - return NodeBuffer.from(text, 'utf-8').toString('base64'); - } - return btoa(unescape(encodeURIComponent(text))); -} - -/** - * Pack a component stack trace string into an attachment. - * - * On web we wrap the text in a `Blob` so FormData sends it as a real file - * part. On React Native we can't use a `Blob` (RN's FormData polyfill can't - * serialize browser Blobs), so we encode the text as a base64 `data:` URI - * inside RN's native `{ uri, type }` file shape instead. - */ -function createComponentStackAttachment( - componentStack: string -): BugSplatAttachment { - if (isReactNative) { - return { - filename: 'componentStack.txt', - data: { - uri: `data:text/plain;base64,${utf8ToBase64(componentStack)}`, - type: 'text/plain', - }, - }; - } - return { - filename: 'componentStack.txt', - data: new Blob([componentStack], { type: 'text/plain' }), - }; -} - export interface FallbackProps { /** * Error that caused crash @@ -176,7 +139,17 @@ interface InternalErrorBoundaryProps { * to pass their own scope that will inject the client for use by * ErrorBoundary. */ - scope: { getClient(): BugSplat | null }; + scope: { + getClient(): BugSplat | null; + /** + * Optional — override how a componentStack string is packaged into an + * attachment. When absent (e.g. a user-supplied duck-typed scope that + * pre-dates this API), ErrorBoundary falls back to the default builder. + */ + getCreateComponentStackAttachment?: () => + | ((componentStack: string) => BugSplatAttachment) + | undefined; + }; } export type ErrorBoundaryProps = JSX.LibraryManagedAttributes< @@ -282,10 +255,13 @@ export class ErrorBoundary extends Component< await beforePost(client, error, componentStack); + const createAttachment = scope.getCreateComponentStackAttachment?.() + ?? defaultCreateComponentStackAttachment; + return client.post(error, { - attachments: [ - createComponentStackAttachment(componentStack), - ], + attachments: componentStack + ? [createAttachment(componentStack)] + : [], }); } diff --git a/src/appScope.ts b/src/appScope.ts index 19249d1..34d0547 100644 --- a/src/appScope.ts +++ b/src/appScope.ts @@ -21,9 +21,14 @@ export interface BugSplatInit { } /** - * Container for managing shared `BugSplat` instance + * Container for managing shared `BugSplat` instance. + * + * Exported so platform-specific wrappers (`@bugsplat/expo`, etc.) can configure + * scope-level behavior — e.g. overriding how `ErrorBoundary` builds the + * component-stack attachment — without having to construct and thread their + * own `Scope` through ``. */ -const appScope: Scope = new Scope(); +export const appScope: Scope = new Scope(); /** * Initialize a new BugSplat instance and store the reference in scope diff --git a/src/index.ts b/src/index.ts index 13832a0..da28709 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ export type { export * from './appScope'; export * from './ErrorBoundary'; +export * from './scope'; export * from './useErrorHandler'; export * from './useFeedback'; export * from './withErrorBoundary'; diff --git a/src/scope.ts b/src/scope.ts index 214c01c..0a5bc57 100644 --- a/src/scope.ts +++ b/src/scope.ts @@ -1,10 +1,43 @@ -import type { BugSplat } from 'bugsplat'; +import type { BugSplat, BugSplatAttachment } from 'bugsplat'; /** - * Encapsulate BugSplat client instance + * Build a `BugSplatAttachment` from a React component stack string. + * + * Runtimes disagree on what FormData can serialize, so the exact shape used + * for the attachment is a runtime concern, not a `bugsplat-react` concern. + * This is the seam `ErrorBoundary` delegates to before posting; override it on + * the scope to emit something other than the web-friendly default (for + * example, React Native can't serialize browser `Blob`s in FormData and needs + * a `{ uri, type }` file-ref instead). + */ +export type CreateComponentStackAttachment = ( + componentStack: string +) => BugSplatAttachment; + +/** + * Default builder — wraps the stack in a `text/plain` `Blob`, which is what + * browser FormData needs to emit a multipart file part. + */ +export const defaultCreateComponentStackAttachment: CreateComponentStackAttachment = ( + componentStack +) => ({ + filename: 'componentStack.txt', + data: new Blob([componentStack], { type: 'text/plain' }), +}); + +/** + * Dependency container read by `ErrorBoundary` at post time. + * + * Holds the active `BugSplat` client plus any platform-varying behavior + * ErrorBoundary needs to delegate (currently: how to turn a component stack + * into an attachment). Runtimes like `@bugsplat/expo` replace the default + * attachment builder with their own in `init()`. */ export class Scope { - constructor(private client: BugSplat | null = null) {} + constructor( + private client: BugSplat | null = null, + private createComponentStackAttachment: CreateComponentStackAttachment = defaultCreateComponentStackAttachment + ) {} /** * @returns BugSplat client instance or null if unset @@ -16,4 +49,12 @@ export class Scope { setClient(client: BugSplat) { this.client = client; } + + getCreateComponentStackAttachment() { + return this.createComponentStackAttachment; + } + + setCreateComponentStackAttachment(fn: CreateComponentStackAttachment) { + this.createComponentStackAttachment = fn; + } } From 89ce3dc3a31145745634f329977ecc448f3ceb22 Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Fri, 17 Apr 2026 21:31:22 -0400 Subject: [PATCH 07/10] refactor: require getCreateComponentStackAttachment on scope prop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` 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) --- spec/ErrorBoundary.spec.tsx | 9 +++------ src/ErrorBoundary.tsx | 23 +++++------------------ 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/spec/ErrorBoundary.spec.tsx b/spec/ErrorBoundary.spec.tsx index 3bd38cc..fc978fd 100644 --- a/spec/ErrorBoundary.spec.tsx +++ b/spec/ErrorBoundary.spec.tsx @@ -87,7 +87,7 @@ describe('', () => { describe('when BugSplat has been initialized', () => { let bugSplat: BugSplat; - let scope: Pick; + let scope: Scope; beforeEach(() => { bugSplat = { @@ -96,7 +96,7 @@ describe('', () => { version: '', post: mockPost, } as unknown as BugSplat; - scope = { getClient: () => bugSplat }; + scope = new Scope(bugSplat); }); it('should call onError', async () => { @@ -172,10 +172,7 @@ describe('', () => { it('honors scope.getCreateComponentStackAttachment() when provided', async () => { const customAttachment = { filename: 'componentStack.txt', data: 'CUSTOM' }; const customBuilder = jest.fn(() => customAttachment); - const scopeWithBuilder = { - getClient: () => bugSplat, - getCreateComponentStackAttachment: () => customBuilder, - }; + const scopeWithBuilder = new Scope(bugSplat, customBuilder); render( diff --git a/src/ErrorBoundary.tsx b/src/ErrorBoundary.tsx index a3e40ce..770a74c 100644 --- a/src/ErrorBoundary.tsx +++ b/src/ErrorBoundary.tsx @@ -11,8 +11,8 @@ import { type ReactElement, type ReactNode, } from 'react'; -import { getBugSplat } from './appScope'; -import { defaultCreateComponentStackAttachment } from './scope'; +import { appScope } from './appScope'; +import type { Scope } from './scope'; /** * Shallowly compare two arrays to determine if they are different. @@ -139,17 +139,7 @@ interface InternalErrorBoundaryProps { * to pass their own scope that will inject the client for use by * ErrorBoundary. */ - scope: { - getClient(): BugSplat | null; - /** - * Optional — override how a componentStack string is packaged into an - * attachment. When absent (e.g. a user-supplied duck-typed scope that - * pre-dates this API), ErrorBoundary falls back to the default builder. - */ - getCreateComponentStackAttachment?: () => - | ((componentStack: string) => BugSplatAttachment) - | undefined; - }; + scope: Pick; } export type ErrorBoundaryProps = JSX.LibraryManagedAttributes< @@ -209,7 +199,7 @@ export class ErrorBoundary extends Component< onResetKeysChange: noop, onUnmount: noop, disablePost: false, - scope: { getClient: getBugSplat }, + scope: appScope, }; state = INITIAL_STATE; @@ -255,12 +245,9 @@ export class ErrorBoundary extends Component< await beforePost(client, error, componentStack); - const createAttachment = scope.getCreateComponentStackAttachment?.() - ?? defaultCreateComponentStackAttachment; - return client.post(error, { attachments: componentStack - ? [createAttachment(componentStack)] + ? [scope.getCreateComponentStackAttachment()(componentStack)] : [], }); } From 954840f6add657d44213765c974a8f487e1bd4ce Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Fri, 17 Apr 2026 21:38:15 -0400 Subject: [PATCH 08/10] docs: trim appScope comment Co-Authored-By: Claude Opus 4.7 (1M context) --- src/appScope.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/appScope.ts b/src/appScope.ts index 34d0547..8acaed2 100644 --- a/src/appScope.ts +++ b/src/appScope.ts @@ -21,12 +21,7 @@ export interface BugSplatInit { } /** - * Container for managing shared `BugSplat` instance. - * - * Exported so platform-specific wrappers (`@bugsplat/expo`, etc.) can configure - * scope-level behavior — e.g. overriding how `ErrorBoundary` builds the - * component-stack attachment — without having to construct and thread their - * own `Scope` through ``. + * Container for the shared `BugSplat` instance and scope-level overrides. */ export const appScope: Scope = new Scope(); From 09c58e9a61ed26dcaddf5cff0a497609e6de34ec Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Fri, 17 Apr 2026 21:47:55 -0400 Subject: [PATCH 09/10] docs: trim scope.ts JSDoc to match house style Co-Authored-By: Claude Opus 4.7 (1M context) --- src/scope.ts | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/src/scope.ts b/src/scope.ts index 0a5bc57..3b303ca 100644 --- a/src/scope.ts +++ b/src/scope.ts @@ -1,22 +1,14 @@ import type { BugSplat, BugSplatAttachment } from 'bugsplat'; /** - * Build a `BugSplatAttachment` from a React component stack string. - * - * Runtimes disagree on what FormData can serialize, so the exact shape used - * for the attachment is a runtime concern, not a `bugsplat-react` concern. - * This is the seam `ErrorBoundary` delegates to before posting; override it on - * the scope to emit something other than the web-friendly default (for - * example, React Native can't serialize browser `Blob`s in FormData and needs - * a `{ uri, type }` file-ref instead). + * Builds the componentStack attachment for ErrorBoundary posts. */ export type CreateComponentStackAttachment = ( componentStack: string ) => BugSplatAttachment; /** - * Default builder — wraps the stack in a `text/plain` `Blob`, which is what - * browser FormData needs to emit a multipart file part. + * Default builder — wraps the stack in a `text/plain` `Blob`. */ export const defaultCreateComponentStackAttachment: CreateComponentStackAttachment = ( componentStack @@ -26,12 +18,7 @@ export const defaultCreateComponentStackAttachment: CreateComponentStackAttachme }); /** - * Dependency container read by `ErrorBoundary` at post time. - * - * Holds the active `BugSplat` client plus any platform-varying behavior - * ErrorBoundary needs to delegate (currently: how to turn a component stack - * into an attachment). Runtimes like `@bugsplat/expo` replace the default - * attachment builder with their own in `init()`. + * Encapsulate BugSplat client instance and scope-level overrides. */ export class Scope { constructor( @@ -50,6 +37,9 @@ export class Scope { this.client = client; } + /** + * @returns the current componentStack attachment builder + */ getCreateComponentStackAttachment() { return this.createComponentStackAttachment; } From 4b4b24cf8f89843d2b1110043db7a2dff0255b29 Mon Sep 17 00:00:00 2001 From: Bobby Galli Date: Fri, 17 Apr 2026 21:59:48 -0400 Subject: [PATCH 10/10] chore: bump .nvmrc to 24 to match CI 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) --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 3c03207..a45fd52 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +24