Skip to content

Commit 0abcf94

Browse files
authored
Merge branch 'main' into renovate/main-@elasticcharts
2 parents 2dc6e08 + a537781 commit 0abcf94

File tree

17 files changed

+355
-146
lines changed

17 files changed

+355
-146
lines changed

packages/shared-ux/error_boundary/lib/telemetry_events.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,25 @@ export const reactFatalErrorSchema = {
2424
optional: false as const,
2525
},
2626
},
27+
component_stack: {
28+
type: 'text' as const,
29+
_meta: {
30+
description: 'Stack trace React component tree',
31+
optional: false as const,
32+
},
33+
},
2734
error_message: {
2835
type: 'keyword' as const,
2936
_meta: {
3037
description: 'Message from the error',
3138
optional: false as const,
3239
},
3340
},
41+
error_stack: {
42+
type: 'text' as const,
43+
_meta: {
44+
description: 'Stack trace from the error object',
45+
optional: false as const,
46+
},
47+
},
3448
};

packages/shared-ux/error_boundary/src/services/error_boundary_services.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ describe('<KibanaErrorBoundaryProvider>', () => {
3434

3535
expect(reportEventSpy).toBeCalledWith('fatal-error-react', {
3636
component_name: 'BadComponent',
37+
component_stack: expect.any(String),
3738
error_message: 'Error: This is an error to show the test user!',
39+
error_stack: expect.any(String),
3840
});
3941
});
4042

@@ -61,7 +63,9 @@ describe('<KibanaErrorBoundaryProvider>', () => {
6163
expect(reportEventSpy2).not.toBeCalled();
6264
expect(reportEventSpy1).toBeCalledWith('fatal-error-react', {
6365
component_name: 'BadComponent',
66+
component_stack: expect.any(String),
6467
error_message: 'Error: This is an error to show the test user!',
68+
error_stack: expect.any(String),
6569
});
6670
});
6771
});

packages/shared-ux/error_boundary/src/services/error_service.test.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ describe('KibanaErrorBoundary Error Service', () => {
2020

2121
it('decorates fatal error object', () => {
2222
const testFatal = new Error('This is an unrecognized and fatal error');
23-
const serviceError = service.registerError(testFatal, {});
23+
const serviceError = service.registerError(testFatal, { componentStack: '' });
2424

2525
expect(serviceError.isFatal).toBe(true);
2626
});
2727

2828
it('decorates recoverable error object', () => {
2929
const testRecoverable = new Error('Could not load chunk blah blah');
3030
testRecoverable.name = 'ChunkLoadError';
31-
const serviceError = service.registerError(testRecoverable, {});
31+
const serviceError = service.registerError(testRecoverable, { componentStack: '' });
3232

3333
expect(serviceError.isFatal).toBe(false);
3434
});
@@ -88,9 +88,36 @@ describe('KibanaErrorBoundary Error Service', () => {
8888
service.registerError(testFatal, errorInfo);
8989

9090
expect(mockDeps.analytics.reportEvent).toHaveBeenCalledTimes(1);
91-
expect(mockDeps.analytics.reportEvent).toHaveBeenCalledWith('fatal-error-react', {
91+
expect(mockDeps.analytics.reportEvent.mock.calls[0][0]).toBe('fatal-error-react');
92+
expect(mockDeps.analytics.reportEvent.mock.calls[0][1]).toMatchObject({
9293
component_name: 'OutrageousMaker',
9394
error_message: 'Error: This is an outrageous and fatal error',
9495
});
9596
});
97+
98+
it('captures component stack trace and error stack trace for telemetry', () => {
99+
jest.resetAllMocks();
100+
const testFatal = new Error('This is an outrageous and fatal error');
101+
102+
const errorInfo = {
103+
componentStack: `
104+
at OutrageousMaker (http://localhost:9001/main.iframe.bundle.js:11616:73)
105+
`,
106+
};
107+
108+
service.registerError(testFatal, errorInfo);
109+
110+
expect(mockDeps.analytics.reportEvent).toHaveBeenCalledTimes(1);
111+
expect(mockDeps.analytics.reportEvent.mock.calls[0][0]).toBe('fatal-error-react');
112+
expect(
113+
String(mockDeps.analytics.reportEvent.mock.calls[0][1].component_stack).includes(
114+
'at OutrageousMaker'
115+
)
116+
).toBe(true);
117+
expect(
118+
String(mockDeps.analytics.reportEvent.mock.calls[0][1].error_stack).startsWith(
119+
'Error: This is an outrageous and fatal error'
120+
)
121+
).toBe(true);
122+
});
96123
});

packages/shared-ux/error_boundary/src/services/error_service.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const MATCH_CHUNK_LOADERROR = /ChunkLoadError/;
1515

1616
interface ErrorServiceError {
1717
error: Error;
18-
errorInfo?: Partial<React.ErrorInfo> | null;
18+
errorInfo?: React.ErrorInfo;
1919
name: string | null;
2020
isFatal: boolean;
2121
}
@@ -48,7 +48,7 @@ export class KibanaErrorService {
4848
/**
4949
* Derive the name of the component that threw the error
5050
*/
51-
private getErrorComponentName(errorInfo: Partial<React.ErrorInfo> | null) {
51+
private getErrorComponentName(errorInfo?: React.ErrorInfo) {
5252
let errorComponentName: string | null = null;
5353
const stackLines = errorInfo?.componentStack?.split('\n');
5454
const errorIndicator = /^ at (\S+).*/;
@@ -75,18 +75,28 @@ export class KibanaErrorService {
7575
/**
7676
* Creates a decorated error object
7777
*/
78-
public registerError(
79-
error: Error,
80-
errorInfo: Partial<React.ErrorInfo> | null
81-
): ErrorServiceError {
78+
public registerError(error: Error, errorInfo?: React.ErrorInfo): ErrorServiceError {
8279
const isFatal = this.getIsFatal(error);
8380
const name = this.getErrorComponentName(errorInfo);
8481

8582
try {
86-
if (isFatal) {
87-
this.analytics?.reportEvent(REACT_FATAL_ERROR_EVENT_TYPE, {
83+
if (isFatal && this.analytics) {
84+
let componentStack = '';
85+
let errorStack = '';
86+
87+
if (errorInfo && errorInfo.componentStack) {
88+
componentStack = errorInfo.componentStack;
89+
}
90+
91+
if (error instanceof Error && typeof error.stack === 'string') {
92+
errorStack = error.stack;
93+
}
94+
95+
this.analytics.reportEvent(REACT_FATAL_ERROR_EVENT_TYPE, {
8896
component_name: name,
97+
component_stack: componentStack,
8998
error_message: error.toString(),
99+
error_stack: errorStack,
90100
});
91101
}
92102
} catch (e) {

packages/shared-ux/error_boundary/src/ui/error_boundary.test.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,33 @@ describe('<KibanaErrorBoundary>', () => {
8787
);
8888
(await findByTestId('clickForErrorBtn')).click();
8989

90-
expect(mockDeps.analytics.reportEvent).toHaveBeenCalledWith('fatal-error-react', {
90+
expect(mockDeps.analytics.reportEvent.mock.calls[0][0]).toBe('fatal-error-react');
91+
expect(mockDeps.analytics.reportEvent.mock.calls[0][1]).toMatchObject({
9192
component_name: 'BadComponent',
9293
error_message: 'Error: This is an error to show the test user!',
9394
});
9495
});
96+
97+
it('captures component and error stack traces in telemetry', async () => {
98+
const mockDeps = {
99+
analytics: { reportEvent: jest.fn() },
100+
};
101+
services.errorService = new KibanaErrorService(mockDeps);
102+
103+
const { findByTestId } = render(
104+
<Template>
105+
<BadComponent />
106+
</Template>
107+
);
108+
(await findByTestId('clickForErrorBtn')).click();
109+
110+
expect(
111+
mockDeps.analytics.reportEvent.mock.calls[0][1].component_stack.includes('at BadComponent')
112+
).toBe(true);
113+
expect(
114+
mockDeps.analytics.reportEvent.mock.calls[0][1].error_stack.startsWith(
115+
'Error: This is an error to show the test user!'
116+
)
117+
).toBe(true);
118+
});
95119
});

packages/shared-ux/error_boundary/src/ui/error_boundary.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class ErrorBoundaryInternal extends React.Component<
4141
};
4242
}
4343

44-
componentDidCatch(error: Error, errorInfo: Partial<React.ErrorInfo>) {
44+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
4545
const { name, isFatal } = this.props.services.errorService.registerError(error, errorInfo);
4646
this.setState(() => {
4747
return { error, errorInfo, componentName: name, isFatal };

x-pack/plugins/index_management/public/application/components/component_templates/component_template_wizard/component_template_create/component_template_create.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@
55
* 2.0.
66
*/
77

8-
import React, { useState, useEffect } from 'react';
8+
import React, { useState, useEffect, useMemo } from 'react';
99
import { RouteComponentProps } from 'react-router-dom';
1010
import { FormattedMessage } from '@kbn/i18n-react';
1111
import { EuiPageSection, EuiSpacer, EuiPageHeader } from '@elastic/eui';
1212

13+
import { useRedirectPath } from '../../../../hooks/redirect_path';
1314
import { breadcrumbService, IndexManagementBreadcrumb } from '../../../../services/breadcrumbs';
1415
import { ComponentTemplateDeserialized } from '../../shared_imports';
1516
import { useComponentTemplatesContext } from '../../component_templates_context';
1617
import { ComponentTemplateForm } from '../component_template_form';
18+
import { useStepFromQueryString } from '../use_step_from_query_string';
19+
import { useDatastreamsRollover } from '../component_template_datastreams_rollover/use_datastreams_rollover';
20+
import { MANAGED_BY_FLEET } from '../../constants';
1721

1822
interface Props {
1923
/**
@@ -28,8 +32,35 @@ export const ComponentTemplateCreate: React.FunctionComponent<RouteComponentProp
2832
}) => {
2933
const [isSaving, setIsSaving] = useState<boolean>(false);
3034
const [saveError, setSaveError] = useState<any>(null);
35+
const redirectTo = useRedirectPath(history);
36+
37+
const { activeStep: defaultActiveStep, updateStep } = useStepFromQueryString(history);
38+
39+
const locationSearchParams = useMemo(() => {
40+
return new URLSearchParams(history.location.search);
41+
}, [history.location.search]);
42+
43+
const defaultValue = useMemo(() => {
44+
if (sourceComponentTemplate) {
45+
return sourceComponentTemplate;
46+
}
47+
48+
const name = locationSearchParams.get('name') ?? '';
49+
const managedBy = locationSearchParams.get('managed_by');
50+
51+
return {
52+
name,
53+
template: {},
54+
_meta: managedBy ? { managed_by: managedBy } : {},
55+
_kbnMeta: {
56+
usedBy: [],
57+
isManaged: false,
58+
},
59+
};
60+
}, [locationSearchParams, sourceComponentTemplate]);
3161

3262
const { api } = useComponentTemplatesContext();
63+
const { showDatastreamRolloverModal } = useDatastreamsRollover();
3364

3465
const onSave = async (componentTemplate: ComponentTemplateDeserialized) => {
3566
const { name } = componentTemplate;
@@ -45,10 +76,11 @@ export const ComponentTemplateCreate: React.FunctionComponent<RouteComponentProp
4576
setSaveError(error);
4677
return;
4778
}
79+
if (componentTemplate._meta?.managed_by === MANAGED_BY_FLEET) {
80+
await showDatastreamRolloverModal(componentTemplate.name);
81+
}
4882

49-
history.push({
50-
pathname: encodeURI(`/component_templates/${encodeURIComponent(name)}`),
51-
});
83+
redirectTo(encodeURI(`/component_templates/${encodeURIComponent(name)}`));
5284
};
5385

5486
const clearSaveError = () => {
@@ -88,7 +120,9 @@ export const ComponentTemplateCreate: React.FunctionComponent<RouteComponentProp
88120
<EuiSpacer size="l" />
89121

90122
<ComponentTemplateForm
91-
defaultValue={sourceComponentTemplate}
123+
defaultActiveWizardSection={defaultActiveStep}
124+
onStepChange={updateStep}
125+
defaultValue={defaultValue}
92126
onSave={onSave}
93127
isSaving={isSaving}
94128
saveError={saveError}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { renderHook } from '@testing-library/react-hooks';
9+
10+
import { useComponentTemplatesContext } from '../../component_templates_context';
11+
12+
import { useDatastreamsRollover } from './use_datastreams_rollover';
13+
14+
jest.mock('../../component_templates_context');
15+
16+
describe('useStepFromQueryString', () => {
17+
beforeEach(() => {
18+
jest.mocked(useComponentTemplatesContext).mockReturnValue({
19+
api: {
20+
getComponentTemplateDatastreams: jest.fn(),
21+
postDataStreamMappingsFromTemplate: jest.fn(),
22+
},
23+
overlays: { openModal: jest.fn() } as any,
24+
} as any);
25+
});
26+
it('should do nothing if there no impacted data_streams', async () => {
27+
jest
28+
.mocked(useComponentTemplatesContext().api.getComponentTemplateDatastreams)
29+
.mockResolvedValue({ data: { data_streams: [] }, error: undefined });
30+
31+
const {
32+
result: {
33+
current: { showDatastreamRolloverModal },
34+
},
35+
} = renderHook(() => useDatastreamsRollover());
36+
37+
await showDatastreamRolloverModal('logs-test.data@custom');
38+
});
39+
40+
it('should try to update mappings if there is impacted data_streams', async () => {
41+
const { api, overlays } = jest.mocked(useComponentTemplatesContext());
42+
43+
api.getComponentTemplateDatastreams.mockResolvedValue({
44+
data: { data_streams: ['logs-test.data-default'] },
45+
error: undefined,
46+
});
47+
48+
api.postDataStreamMappingsFromTemplate.mockResolvedValue({
49+
error: undefined,
50+
data: { data_streams: [] },
51+
});
52+
53+
jest
54+
.mocked(useComponentTemplatesContext().overlays.openModal)
55+
.mockReturnValue({ onClose: jest.fn() } as any);
56+
57+
const {
58+
result: {
59+
current: { showDatastreamRolloverModal },
60+
},
61+
} = renderHook(() => useDatastreamsRollover());
62+
63+
await showDatastreamRolloverModal('logs-test.data@custom');
64+
65+
expect(api.postDataStreamMappingsFromTemplate).toBeCalledTimes(1);
66+
expect(overlays.openModal).not.toBeCalled();
67+
});
68+
69+
it('should show datastream rollover modal if there is an error when updating mappings', async () => {
70+
const { api, overlays } = jest.mocked(useComponentTemplatesContext());
71+
72+
api.getComponentTemplateDatastreams.mockResolvedValue({
73+
data: { data_streams: ['logs-test.data-default'] },
74+
error: undefined,
75+
});
76+
77+
api.postDataStreamMappingsFromTemplate.mockResolvedValue({
78+
error: new Error('test'),
79+
data: { data_streams: [] },
80+
});
81+
82+
jest
83+
.mocked(useComponentTemplatesContext().overlays.openModal)
84+
.mockReturnValue({ onClose: jest.fn() } as any);
85+
86+
const {
87+
result: {
88+
current: { showDatastreamRolloverModal },
89+
},
90+
} = renderHook(() => useDatastreamsRollover());
91+
92+
await showDatastreamRolloverModal('logs-test.data@custom');
93+
expect(overlays.openModal).toBeCalled();
94+
});
95+
});

0 commit comments

Comments
 (0)