/
errorboundary.tsx
149 lines (128 loc) · 4.82 KB
/
errorboundary.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import { captureException, ReportDialogOptions, Scope, showReportDialog, withScope } from '@sentry/browser';
import * as hoistNonReactStatic from 'hoist-non-react-statics';
import * as React from 'react';
export const UNKNOWN_COMPONENT = 'unknown';
export type FallbackRender = (fallback: {
error: Error | null;
componentStack: string | null;
resetError(): void;
eventId: string | null;
}) => React.ReactNode;
export type ErrorBoundaryProps = {
/** If a Sentry report dialog should be rendered on error */
showDialog?: boolean;
/**
* Options to be passed into the Sentry report dialog.
* No-op if {@link showDialog} is false.
*/
dialogOptions?: ReportDialogOptions;
// tslint:disable no-null-undefined-union
/**
* A fallback component that gets rendered when the error boundary encounters an error.
*
* Can either provide a React Component, or a function that returns React Component as
* a valid fallback prop. If a function is provided, the function will be called with
* the error, the component stack, and an function that resets the error boundary on error.
*
*/
fallback?: React.ReactNode | FallbackRender;
// tslint:enable no-null-undefined-union
/** Called with the error boundary encounters an error */
onError?(error: Error, componentStack: string, eventId: string): void;
/** Called on componentDidMount() */
onMount?(): void;
/** Called if resetError() is called from the fallback render props function */
onReset?(error: Error | null, componentStack: string | null, eventId: string | null): void;
/** Called on componentWillUnmount() */
onUnmount?(error: Error | null, componentStack: string | null, eventId: string | null): void;
/** Called before error is sent to Sentry, allows for you to add tags or context using the scope */
beforeCapture?(scope: Scope, error: Error | null, componentStack: string | null): void;
};
type ErrorBoundaryState = {
componentStack: string | null;
error: Error | null;
eventId: string | null;
};
const INITIAL_STATE = {
componentStack: null,
error: null,
eventId: null,
};
/**
* A ErrorBoundary component that logs errors to Sentry.
* Requires React >= 16
*/
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
public state: ErrorBoundaryState = INITIAL_STATE;
public componentDidCatch(error: Error, { componentStack }: React.ErrorInfo): void {
const { beforeCapture, onError, showDialog, dialogOptions } = this.props;
withScope(scope => {
if (beforeCapture) {
beforeCapture(scope, error, componentStack);
}
const eventId = captureException(error, { contexts: { react: { componentStack } } });
if (onError) {
onError(error, componentStack, eventId);
}
if (showDialog) {
showReportDialog({ ...dialogOptions, eventId });
}
// componentDidCatch is used over getDerivedStateFromError
// so that componentStack is accessible through state.
this.setState({ error, componentStack, eventId });
});
}
public componentDidMount(): void {
const { onMount } = this.props;
if (onMount) {
onMount();
}
}
public componentWillUnmount(): void {
const { error, componentStack, eventId } = this.state;
const { onUnmount } = this.props;
if (onUnmount) {
onUnmount(error, componentStack, eventId);
}
}
public resetErrorBoundary = () => {
const { onReset } = this.props;
const { error, componentStack, eventId } = this.state;
if (onReset) {
onReset(error, componentStack, eventId);
}
this.setState(INITIAL_STATE);
};
public render(): React.ReactNode {
const { fallback } = this.props;
const { error, componentStack, eventId } = this.state;
if (error) {
if (React.isValidElement(fallback)) {
return fallback;
}
if (typeof fallback === 'function') {
return fallback({ error, componentStack, resetError: this.resetErrorBoundary, eventId }) as FallbackRender;
}
// Fail gracefully if no fallback provided
return null;
}
return this.props.children;
}
}
function withErrorBoundary<P extends object>(
WrappedComponent: React.ComponentType<P>,
errorBoundaryOptions: ErrorBoundaryProps,
): React.FC<P> {
const componentDisplayName = WrappedComponent.displayName || WrappedComponent.name || UNKNOWN_COMPONENT;
const Wrapped: React.FC<P> = (props: P) => (
<ErrorBoundary {...errorBoundaryOptions}>
<WrappedComponent {...props} />
</ErrorBoundary>
);
Wrapped.displayName = `errorBoundary(${componentDisplayName})`;
// Copy over static methods from Wrapped component to Profiler HOC
// See: https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over
hoistNonReactStatic(Wrapped, WrappedComponent);
return Wrapped;
}
export { ErrorBoundary, withErrorBoundary };