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
602 changes: 588 additions & 14 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export { BacktraceReport } from '@backtrace/sdk-core';
export * from '@backtrace/sdk-core/lib/model/attachment';
export * from './BacktraceClient';
export * from './BacktraceConfiguration';
export * from './builder/BacktraceClientBuilder';
export * from './agentDefinition';
5 changes: 5 additions & 0 deletions packages/react/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
};
11 changes: 10 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"clean": "rimraf \"lib\"",
"format": "prettier --write '**/*.ts'",
"lint": "eslint . --ext .ts",
"watch": "webpack -w"
"watch": "webpack -w",
"test": "NODE_ENV=test jest"
},
"repository": {
"type": "git",
Expand All @@ -31,6 +32,14 @@
},
"homepage": "https://github.com/backtrace-labs/backtrace-javascript#readme",
"devDependencies": {
"@testing-library/react": "^14.0.0",
"@types/react": "^18.2.14",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"ts-jest": "^29.1.1",
"typescript": "^5.0.4"
},
"peerDependencies": {
"react": ">=16.14.0"
}
}
37 changes: 37 additions & 0 deletions packages/react/src/BacktraceClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
BacktraceAttributeProvider,
BacktraceCoreClient,
BacktraceRequestHandler,
BacktraceStackTraceConverter,
} from '@backtrace/sdk-core';
import { AGENT } from '@backtrace/browser';
import { BacktraceConfiguration } from '@backtrace/browser';
import { BacktraceClientBuilder } from '@backtrace/browser';

export class BacktraceClient extends BacktraceCoreClient {
private static _instance?: BacktraceClient;
constructor(
options: BacktraceConfiguration,
handler: BacktraceRequestHandler,
attributeProviders: BacktraceAttributeProvider[],
stackTraceConverter: BacktraceStackTraceConverter,
) {
super(options, AGENT, handler, attributeProviders, stackTraceConverter);
}

public static builder(options: BacktraceConfiguration): BacktraceClientBuilder {
return new BacktraceClientBuilder(options);
}

public static initialize(options: BacktraceConfiguration): BacktraceClient {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need to use the initialize method? Could you take a look at you're calling builder to build the class and store it in the class instance of the client? You can do the same in the constructor, which is a more intuitive way for me.

WIth this approach, you have one way to initialize everything. Right now, we have a different way to initialize other clients and we don't know which one (builder vs client) is preferred.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I propose we add an initialize method to all of our clients that calls the builder. I think it would provide a good user experience as the user would not be required to know implementation details about which patterns we use to create the client.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we call it "implementation details" to use BacktraceBuilder.build(options)? I think it's really common pattern. I'm really against static initialize method that wraps builder. Sooner than later we end up in a situation we can't do something here.

I think we should discuss this as a team. I don't like the idea of having 3 ways to initialize the client. It's a short way to generate a lot of problems.

Copy link
Contributor

Choose a reason for hiding this comment

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

In some way I agree with @adamcronin42 that .initialize may be a better method. Or the one that actually initializes Backtrace in a global context. .build could only create the instance, and not assign it as a global error handler.

this._instance = this.builder(options).build();
return this._instance;
}

public static get instance(): BacktraceClient {
if (!this._instance) {
throw new Error('BacktraceClient is uninitialized. Call "BacktraceClient.initialize" function first.');
}
return this._instance;
}
}
52 changes: 52 additions & 0 deletions packages/react/src/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Component, ErrorInfo, ReactElement, ReactNode, isValidElement } from 'react';
import { BacktraceClient } from './BacktraceClient';

type RenderFallback = () => ReactElement;

export interface Props {
children: ReactNode;
fallback?: ReactElement | RenderFallback;
}

export interface State {
hasError: boolean;
error?: Error;
}

export class ErrorBoundary extends Component<Props, State> {
private _client: BacktraceClient;
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: undefined,
};
// grabbing here so it will fail fast if BacktraceClient is uninitialized
this._client = BacktraceClient.instance;
}

static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}

componentDidCatch(error: Error, info: ErrorInfo) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

shouldn't we also add accessors like public or private? We use it everywhere in the code. Is there any reason why we shouldn't continue doing that here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call I can add them

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in latest PR

this._client.send(error);
}

render() {
const { fallback, children } = this.props;

if (!this.state.hasError) {
return children;
}

const fallbackComponent = typeof fallback === 'function' ? fallback() : fallback;

if (fallbackComponent && isValidElement(fallbackComponent)) {
return fallbackComponent;
}

// no or invalid fallback
return null;
}
}
2 changes: 2 additions & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from '@backtrace/browser';
export * from './ErrorBoundary';
export { BacktraceClient } from './BacktraceClient';
86 changes: 86 additions & 0 deletions packages/react/tests/errorBoundaryTests.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { ErrorBoundary } from '../src/ErrorBoundary';
import { BacktraceClient } from '../src/BacktraceClient';

describe('Error Boundary', () => {
const childrenText = 'I am the children';
const fallbackText = 'This is a fallback';
const errorText = 'Rendering error!';

function ValidComponent() {
return <p>{childrenText}</p>;
}

function ErrorComponent() {
throw new Error(errorText);
return <p>{childrenText}</p>;
}

function Fallback() {
return <p>{fallbackText}</p>;
}

const fallbackFunction = () => <Fallback />;

describe('With BacktraeClient uninitialized', () => {
it('Should throw an error when BacktraceClient is uninitialized and an ErrorBoundary is used', () => {
expect(() =>
render(<ErrorBoundary fallback={<Fallback />}>{<ErrorComponent />}</ErrorBoundary>),
).toThrowError();
});
});

describe('With BacktraceClient initialized', () => {
let client: BacktraceClient;
beforeEach(() => {
client = BacktraceClient.initialize({
url: `https://submit.backtrace.io/universe/token/json`,
name: 'test',
version: '1.0.0',
});
});

it('Should not throw an error when BacktraceClient is initialized and an ErrorBoundary is used', () => {
expect(() =>
render(<ErrorBoundary fallback={Fallback}>{<ValidComponent />}</ErrorBoundary>),
).not.toThrowError();
});

it('Should render children', () => {
render(<ErrorBoundary fallback={Fallback}>{<ValidComponent />}</ErrorBoundary>);
expect(screen.getByText(childrenText));
});

it('Should render fallback function on rendering error', () => {
render(<ErrorBoundary fallback={fallbackFunction}>{<ErrorComponent />}</ErrorBoundary>);
expect(screen.getByText(fallbackText));
});

it('Should render fallback component on rendering error', () => {
render(<ErrorBoundary fallback={<Fallback />}>{<ErrorComponent />}</ErrorBoundary>);
expect(screen.getByText(fallbackText));
});

it('Should render nothing if no fallback is passed in and rendering error', () => {
const { container } = render(
<ErrorBoundary>
<ErrorComponent />
</ErrorBoundary>,
);
expect(container.firstChild).toBeNull();
});

it('Should send to Backtrace on rendering error', () => {
const clientSpy = jest.spyOn(client, 'send');
render(<ErrorBoundary fallback={<Fallback />}>{<ErrorComponent />}</ErrorBoundary>);
expect(clientSpy).toHaveBeenCalled();
});

it('Should not send to Backtrace when no rendering error occurs', () => {
const clientSpy = jest.spyOn(client, 'send');
render(<ErrorBoundary fallback={<Fallback />}>{<ValidComponent />}</ErrorBoundary>);
expect(clientSpy).not.toHaveBeenCalled();
});
});
});
3 changes: 2 additions & 1 deletion packages/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib"
"outDir": "./lib",
"jsx": "react"
},
"exclude": ["node_modules", "tests", "lib"],
"references": [
Expand Down