mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
Error boundary telemetry stacktraces 2 (#175219)
## Summary Closes https://github.com/elastic/kibana-team/issues/733 This change adds two new fields to [Error Boundary event-based telemetry](https://github.com/elastic/kibana/pull/169895) logging: `error_stack` and `component_stack`. - `error_stack` is a stacktrace of the error message that was caught, if any, otherwise an empty string. - `component_stack` is the list React components in the React component tree where error propagated until it was caught by the Error Boundary. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
48f8653279
commit
7490c32373
6 changed files with 92 additions and 13 deletions
|
@ -24,6 +24,13 @@ export const reactFatalErrorSchema = {
|
|||
optional: false as const,
|
||||
},
|
||||
},
|
||||
component_stack: {
|
||||
type: 'text' as const,
|
||||
_meta: {
|
||||
description: 'Stack trace React component tree',
|
||||
optional: false as const,
|
||||
},
|
||||
},
|
||||
error_message: {
|
||||
type: 'keyword' as const,
|
||||
_meta: {
|
||||
|
@ -31,4 +38,11 @@ export const reactFatalErrorSchema = {
|
|||
optional: false as const,
|
||||
},
|
||||
},
|
||||
error_stack: {
|
||||
type: 'text' as const,
|
||||
_meta: {
|
||||
description: 'Stack trace from the error object',
|
||||
optional: false as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -34,7 +34,9 @@ describe('<KibanaErrorBoundaryProvider>', () => {
|
|||
|
||||
expect(reportEventSpy).toBeCalledWith('fatal-error-react', {
|
||||
component_name: 'BadComponent',
|
||||
component_stack: expect.any(String),
|
||||
error_message: 'Error: This is an error to show the test user!',
|
||||
error_stack: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -61,7 +63,9 @@ describe('<KibanaErrorBoundaryProvider>', () => {
|
|||
expect(reportEventSpy2).not.toBeCalled();
|
||||
expect(reportEventSpy1).toBeCalledWith('fatal-error-react', {
|
||||
component_name: 'BadComponent',
|
||||
component_stack: expect.any(String),
|
||||
error_message: 'Error: This is an error to show the test user!',
|
||||
error_stack: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ describe('KibanaErrorBoundary Error Service', () => {
|
|||
|
||||
it('decorates fatal error object', () => {
|
||||
const testFatal = new Error('This is an unrecognized and fatal error');
|
||||
const serviceError = service.registerError(testFatal, {});
|
||||
const serviceError = service.registerError(testFatal, { componentStack: '' });
|
||||
|
||||
expect(serviceError.isFatal).toBe(true);
|
||||
});
|
||||
|
@ -28,7 +28,7 @@ describe('KibanaErrorBoundary Error Service', () => {
|
|||
it('decorates recoverable error object', () => {
|
||||
const testRecoverable = new Error('Could not load chunk blah blah');
|
||||
testRecoverable.name = 'ChunkLoadError';
|
||||
const serviceError = service.registerError(testRecoverable, {});
|
||||
const serviceError = service.registerError(testRecoverable, { componentStack: '' });
|
||||
|
||||
expect(serviceError.isFatal).toBe(false);
|
||||
});
|
||||
|
@ -88,9 +88,36 @@ describe('KibanaErrorBoundary Error Service', () => {
|
|||
service.registerError(testFatal, errorInfo);
|
||||
|
||||
expect(mockDeps.analytics.reportEvent).toHaveBeenCalledTimes(1);
|
||||
expect(mockDeps.analytics.reportEvent).toHaveBeenCalledWith('fatal-error-react', {
|
||||
expect(mockDeps.analytics.reportEvent.mock.calls[0][0]).toBe('fatal-error-react');
|
||||
expect(mockDeps.analytics.reportEvent.mock.calls[0][1]).toMatchObject({
|
||||
component_name: 'OutrageousMaker',
|
||||
error_message: 'Error: This is an outrageous and fatal error',
|
||||
});
|
||||
});
|
||||
|
||||
it('captures component stack trace and error stack trace for telemetry', () => {
|
||||
jest.resetAllMocks();
|
||||
const testFatal = new Error('This is an outrageous and fatal error');
|
||||
|
||||
const errorInfo = {
|
||||
componentStack: `
|
||||
at OutrageousMaker (http://localhost:9001/main.iframe.bundle.js:11616:73)
|
||||
`,
|
||||
};
|
||||
|
||||
service.registerError(testFatal, errorInfo);
|
||||
|
||||
expect(mockDeps.analytics.reportEvent).toHaveBeenCalledTimes(1);
|
||||
expect(mockDeps.analytics.reportEvent.mock.calls[0][0]).toBe('fatal-error-react');
|
||||
expect(
|
||||
String(mockDeps.analytics.reportEvent.mock.calls[0][1].component_stack).includes(
|
||||
'at OutrageousMaker'
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
String(mockDeps.analytics.reportEvent.mock.calls[0][1].error_stack).startsWith(
|
||||
'Error: This is an outrageous and fatal error'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ const MATCH_CHUNK_LOADERROR = /ChunkLoadError/;
|
|||
|
||||
interface ErrorServiceError {
|
||||
error: Error;
|
||||
errorInfo?: Partial<React.ErrorInfo> | null;
|
||||
errorInfo?: React.ErrorInfo;
|
||||
name: string | null;
|
||||
isFatal: boolean;
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export class KibanaErrorService {
|
|||
/**
|
||||
* Derive the name of the component that threw the error
|
||||
*/
|
||||
private getErrorComponentName(errorInfo: Partial<React.ErrorInfo> | null) {
|
||||
private getErrorComponentName(errorInfo?: React.ErrorInfo) {
|
||||
let errorComponentName: string | null = null;
|
||||
const stackLines = errorInfo?.componentStack?.split('\n');
|
||||
const errorIndicator = /^ at (\S+).*/;
|
||||
|
@ -75,18 +75,28 @@ export class KibanaErrorService {
|
|||
/**
|
||||
* Creates a decorated error object
|
||||
*/
|
||||
public registerError(
|
||||
error: Error,
|
||||
errorInfo: Partial<React.ErrorInfo> | null
|
||||
): ErrorServiceError {
|
||||
public registerError(error: Error, errorInfo?: React.ErrorInfo): ErrorServiceError {
|
||||
const isFatal = this.getIsFatal(error);
|
||||
const name = this.getErrorComponentName(errorInfo);
|
||||
|
||||
try {
|
||||
if (isFatal) {
|
||||
this.analytics?.reportEvent(REACT_FATAL_ERROR_EVENT_TYPE, {
|
||||
if (isFatal && this.analytics) {
|
||||
let componentStack = '';
|
||||
let errorStack = '';
|
||||
|
||||
if (errorInfo && errorInfo.componentStack) {
|
||||
componentStack = errorInfo.componentStack;
|
||||
}
|
||||
|
||||
if (error instanceof Error && typeof error.stack === 'string') {
|
||||
errorStack = error.stack;
|
||||
}
|
||||
|
||||
this.analytics.reportEvent(REACT_FATAL_ERROR_EVENT_TYPE, {
|
||||
component_name: name,
|
||||
component_stack: componentStack,
|
||||
error_message: error.toString(),
|
||||
error_stack: errorStack,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
|
@ -87,9 +87,33 @@ describe('<KibanaErrorBoundary>', () => {
|
|||
);
|
||||
(await findByTestId('clickForErrorBtn')).click();
|
||||
|
||||
expect(mockDeps.analytics.reportEvent).toHaveBeenCalledWith('fatal-error-react', {
|
||||
expect(mockDeps.analytics.reportEvent.mock.calls[0][0]).toBe('fatal-error-react');
|
||||
expect(mockDeps.analytics.reportEvent.mock.calls[0][1]).toMatchObject({
|
||||
component_name: 'BadComponent',
|
||||
error_message: 'Error: This is an error to show the test user!',
|
||||
});
|
||||
});
|
||||
|
||||
it('captures component and error stack traces in telemetry', async () => {
|
||||
const mockDeps = {
|
||||
analytics: { reportEvent: jest.fn() },
|
||||
};
|
||||
services.errorService = new KibanaErrorService(mockDeps);
|
||||
|
||||
const { findByTestId } = render(
|
||||
<Template>
|
||||
<BadComponent />
|
||||
</Template>
|
||||
);
|
||||
(await findByTestId('clickForErrorBtn')).click();
|
||||
|
||||
expect(
|
||||
mockDeps.analytics.reportEvent.mock.calls[0][1].component_stack.includes('at BadComponent')
|
||||
).toBe(true);
|
||||
expect(
|
||||
mockDeps.analytics.reportEvent.mock.calls[0][1].error_stack.startsWith(
|
||||
'Error: This is an error to show the test user!'
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -41,7 +41,7 @@ class ErrorBoundaryInternal extends React.Component<
|
|||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: Partial<React.ErrorInfo>) {
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
const { name, isFatal } = this.props.services.errorService.registerError(error, errorInfo);
|
||||
this.setState(() => {
|
||||
return { error, errorInfo, componentName: name, isFatal };
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue