Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
bobbyg603 marked this conversation as resolved.
cache: 'npm'

- name: Install Dependencies
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
bobbyg603 marked this conversation as resolved.
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18
24
277 changes: 135 additions & 142 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
39 changes: 37 additions & 2 deletions spec/ErrorBoundary.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('<ErrorBoundary />', () => {

describe('when BugSplat has been initialized', () => {
let bugSplat: BugSplat;
let scope: Pick<Scope, 'getClient'>;
let scope: Scope;

beforeEach(() => {
bugSplat = {
Expand All @@ -96,7 +96,7 @@ describe('<ErrorBoundary />', () => {
version: '',
post: mockPost,
} as unknown as BugSplat;
scope = { getClient: () => bugSplat };
scope = new Scope(bugSplat);
});

it('should call onError', async () => {
Expand Down Expand Up @@ -152,6 +152,41 @@ describe('<ErrorBoundary />', () => {
await waitFor(() => expect(mockPost).toHaveBeenCalledTimes(1));
});

it('should attach componentStack as a text/plain Blob by default', async () => {
render(
<ErrorBoundary scope={scope} fallback={BasicFallback}>
<BlowUp />
</ErrorBoundary>
);

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(attachment.data).toBeInstanceOf(Blob);
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 = new Scope(bugSplat, customBuilder);

render(
<ErrorBoundary scope={scopeWithBuilder} fallback={BasicFallback}>
<BlowUp />
</ErrorBoundary>
);

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(
<ErrorBoundary
Expand Down
25 changes: 7 additions & 18 deletions src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
type ReactElement,
type ReactNode,
} from 'react';
import { getBugSplat } from './appScope';
import { appScope } from './appScope';
import type { Scope } from './scope';

/**
* Shallowly compare two arrays to determine if they are different.
Expand All @@ -28,18 +29,6 @@ function isArrayDiff(a: unknown[] = [], b: unknown[] = []) {
return a.some((item, index) => !Object.is(item, b[index]));
}

/**
* Pack a component stack trace string into an attachment
*/
function createComponentStackAttachment(
componentStack: string
): BugSplatAttachment {
return {
filename: 'componentStack.txt',
data: new Blob([componentStack]),
};
}

export interface FallbackProps {
/**
* Error that caused crash
Expand Down Expand Up @@ -150,7 +139,7 @@ interface InternalErrorBoundaryProps {
* to pass their own scope that will inject the client for use by
* ErrorBoundary.
*/
scope: { getClient(): BugSplat | null };
scope: Pick<Scope, 'getClient' | 'getCreateComponentStackAttachment'>;
Comment thread
bobbyg603 marked this conversation as resolved.
}

export type ErrorBoundaryProps = JSX.LibraryManagedAttributes<
Expand Down Expand Up @@ -210,7 +199,7 @@ export class ErrorBoundary extends Component<
onResetKeysChange: noop,
onUnmount: noop,
disablePost: false,
scope: { getClient: getBugSplat },
scope: appScope,
};

state = INITIAL_STATE;
Expand Down Expand Up @@ -257,9 +246,9 @@ export class ErrorBoundary extends Component<
await beforePost(client, error, componentStack);

return client.post(error, {
attachments: [
createComponentStackAttachment(componentStack),
],
attachments: componentStack
? [scope.getCreateComponentStackAttachment()(componentStack)]
: [],
Comment thread
bobbyg603 marked this conversation as resolved.
});
}

Expand Down
4 changes: 2 additions & 2 deletions src/appScope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export interface BugSplatInit {
}

/**
* Container for managing shared `BugSplat` instance
* Container for the shared `BugSplat` instance and scope-level overrides.
*/
const appScope: Scope = new Scope();
export const appScope: Scope = new Scope();

/**
* Initialize a new BugSplat instance and store the reference in scope
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type {

export * from './appScope';
export * from './ErrorBoundary';
export * from './scope';
export * from './useErrorHandler';
export * from './useFeedback';
export * from './withErrorBoundary';
37 changes: 34 additions & 3 deletions src/scope.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import type { BugSplat } from 'bugsplat';
import type { BugSplat, BugSplatAttachment } from 'bugsplat';

/**
* Encapsulate BugSplat client instance
* Builds the componentStack attachment for ErrorBoundary posts.
*/
export type CreateComponentStackAttachment = (
componentStack: string
) => BugSplatAttachment;

/**
* Default builder — wraps the stack in a `text/plain` `Blob`.
*/
export const defaultCreateComponentStackAttachment: CreateComponentStackAttachment = (
componentStack
) => ({
filename: 'componentStack.txt',
data: new Blob([componentStack], { type: 'text/plain' }),
Comment thread
bobbyg603 marked this conversation as resolved.
});
Comment thread
bobbyg603 marked this conversation as resolved.

/**
* Encapsulate BugSplat client instance and scope-level overrides.
*/
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
Expand All @@ -16,4 +36,15 @@ export class Scope {
setClient(client: BugSplat) {
this.client = client;
}

/**
* @returns the current componentStack attachment builder
*/
getCreateComponentStackAttachment() {
return this.createComponentStackAttachment;
}

setCreateComponentStackAttachment(fn: CreateComponentStackAttachment) {
this.createComponentStackAttachment = fn;
}
}
Loading