mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Integrate Event-Based Telemetry in KibanaErrorBoundary (#169895)
## Summary Part of https://github.com/elastic/kibana-team/issues/646 Depends on https://github.com/elastic/kibana/pull/169324 Implements telemetry for fatal errors caught by KibanaErrorBoundary in: - `packages/core/application/core-application-browser-internal/src/ui/app_router.tsx` - `packages/kbn-shared-ux-utility/src/with_suspense.tsx` [*] - `packages/react/kibana_context/render/render_provider.tsx` [*] - `src/plugins/management/public/components/management_app/management_router.tsx` - `x-pack/plugins/observability_shared/public/components/page_template/page_template.tsx` - `x-pack/plugins/security_solution/public/app/app.tsx` [*] The changes made to these allowed the `analytics` dependency to be provided optionally, to avoid a breaking API change for maintainers. ## Logging screenshot You can trigger a fatal error in the new error boundary component in most places in Kibana by adding a TypeError to a React component: `<p>{breakHere()}</p>` <img width="1586" alt="fatal error telemetry console log" src="97f973ac
-bb25-41f2-bfe2-547a23f2f450"> ## Telemetry work info Dashboard: <img width="1382" alt="image" src="4fe5353a
-61ba-405a-ac18-0dd6a044c182"> Discover: <img width="1331" alt="image" src="2161b552
-c441-4b7c-adef-25896147c08a"> ### Checklist - [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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
8716f65922
commit
704e080c93
72 changed files with 1384 additions and 119 deletions
|
@ -53,6 +53,7 @@ describe('ApplicationService', () => {
|
|||
overlays: overlayServiceMock.createStartContract(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
customBranding: customBrandingServiceMock.createStartContract(),
|
||||
analytics: analyticsServiceMock.createAnalyticsServiceStart(),
|
||||
};
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
|
|
@ -10,8 +10,10 @@ import React from 'react';
|
|||
import { BehaviorSubject } from 'rxjs';
|
||||
import { createMemoryHistory, History, createHashHistory } from 'history';
|
||||
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { AppStatus } from '@kbn/core-application-browser';
|
||||
|
||||
import { AppRouter, AppNotFound } from '../src/ui';
|
||||
import { MockedMounterMap, MockedMounterTuple } from '../src/test_helpers/test_types';
|
||||
import { createRenderer, createAppMounter, getUnmounter } from './utils';
|
||||
|
@ -42,9 +44,12 @@ describe('AppRouter', () => {
|
|||
);
|
||||
};
|
||||
|
||||
const mockAnalytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
|
||||
const createMountersRenderer = () =>
|
||||
createRenderer(
|
||||
<AppRouter
|
||||
analytics={mockAnalytics}
|
||||
history={globalHistory}
|
||||
mounters={mockMountersToMounters()}
|
||||
appStatuses$={mountersToAppStatus$()}
|
||||
|
|
|
@ -2,6 +2,20 @@
|
|||
|
||||
exports[`#start() getComponent returns renderable JSX tree 1`] = `
|
||||
<AppRouter
|
||||
analytics={
|
||||
Object {
|
||||
"optIn": [MockFunction],
|
||||
"reportEvent": [MockFunction],
|
||||
"telemetryCounter$": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
appStatuses$={
|
||||
AnonymousSubject {
|
||||
"closed": false,
|
||||
|
|
|
@ -65,6 +65,7 @@ describe('#setup()', () => {
|
|||
overlays: overlayServiceMock.createStartContract(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
customBranding: customBrandingServiceMock.createStartContract(),
|
||||
analytics: analyticsServiceMock.createAnalyticsServiceStart(),
|
||||
};
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
@ -515,6 +516,7 @@ describe('#start()', () => {
|
|||
overlays: overlayServiceMock.createStartContract(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
customBranding: customBrandingServiceMock.createStartContract(),
|
||||
analytics: analyticsServiceMock.createAnalyticsServiceStart(),
|
||||
};
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
@ -1227,6 +1229,7 @@ describe('#stop()', () => {
|
|||
overlays: overlayServiceMock.createStartContract(),
|
||||
theme: themeServiceMock.createStartContract(),
|
||||
customBranding: customBrandingServiceMock.createStartContract(),
|
||||
analytics: analyticsServiceMock.createAnalyticsServiceStart(),
|
||||
};
|
||||
service = new ApplicationService();
|
||||
});
|
||||
|
|
|
@ -17,7 +17,7 @@ import type { HttpSetup, HttpStart } from '@kbn/core-http-browser';
|
|||
import type { Capabilities } from '@kbn/core-capabilities-common';
|
||||
import type { MountPoint } from '@kbn/core-mount-utils-browser';
|
||||
import type { OverlayStart } from '@kbn/core-overlays-browser';
|
||||
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
|
||||
import type { AnalyticsServiceSetup, AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type {
|
||||
App,
|
||||
AppDeepLink,
|
||||
|
@ -55,6 +55,7 @@ export interface SetupDeps {
|
|||
|
||||
export interface StartDeps {
|
||||
http: HttpStart;
|
||||
analytics: AnalyticsServiceStart;
|
||||
theme: ThemeServiceStart;
|
||||
overlays: OverlayStart;
|
||||
customBranding: CustomBrandingStart;
|
||||
|
@ -225,6 +226,7 @@ export class ApplicationService {
|
|||
}
|
||||
|
||||
public async start({
|
||||
analytics,
|
||||
http,
|
||||
overlays,
|
||||
theme,
|
||||
|
@ -364,6 +366,7 @@ export class ApplicationService {
|
|||
}
|
||||
return (
|
||||
<AppRouter
|
||||
analytics={analytics}
|
||||
history={this.history}
|
||||
theme$={theme.theme$}
|
||||
mounters={availableMounters}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { act } from 'react-dom/test-utils';
|
|||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { type AppMountParameters, AppStatus } from '@kbn/core-application-browser';
|
||||
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
|
||||
import { AppContainer } from './app_container';
|
||||
|
@ -211,19 +212,20 @@ describe('AppContainer', () => {
|
|||
|
||||
it('should call setIsMounting(false) if mounting throws', async () => {
|
||||
const [waitPromise, resolvePromise] = createResolver();
|
||||
const analytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
const mounter = {
|
||||
appBasePath: '/base-path/some-route',
|
||||
appRoute: '/some-route',
|
||||
unmountBeforeMounting: false,
|
||||
exactRoute: false,
|
||||
mount: async ({ element }: AppMountParameters) => {
|
||||
mount: async () => {
|
||||
await waitPromise;
|
||||
throw new Error(`Mounting failed!`);
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundaryProvider analytics={analytics}>
|
||||
<KibanaErrorBoundary>
|
||||
<AppContainer
|
||||
appPath={`/app/${appId}`}
|
||||
|
|
|
@ -17,11 +17,13 @@ import type { CoreTheme } from '@kbn/core-theme-browser';
|
|||
import type { MountPoint } from '@kbn/core-mount-utils-browser';
|
||||
import { type AppLeaveHandler, AppStatus } from '@kbn/core-application-browser';
|
||||
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type { Mounter } from '../types';
|
||||
import { AppContainer } from './app_container';
|
||||
import { CoreScopedHistory } from '../scoped_history';
|
||||
|
||||
interface Props {
|
||||
analytics: AnalyticsServiceStart;
|
||||
mounters: Map<string, Mounter>;
|
||||
history: History;
|
||||
theme$: Observable<CoreTheme>;
|
||||
|
@ -38,6 +40,7 @@ interface Params {
|
|||
|
||||
export const AppRouter: FunctionComponent<Props> = ({
|
||||
history,
|
||||
analytics,
|
||||
mounters,
|
||||
theme$,
|
||||
setAppLeaveHandler,
|
||||
|
@ -55,7 +58,7 @@ export const AppRouter: FunctionComponent<Props> = ({
|
|||
const showPlainSpinner = useObservable(hasCustomBranding$ ?? EMPTY, false);
|
||||
|
||||
return (
|
||||
<KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundaryProvider analytics={analytics}>
|
||||
<KibanaErrorBoundary>
|
||||
<Router history={history}>
|
||||
<Routes>
|
||||
|
|
|
@ -4,6 +4,13 @@ exports[`#add() deletes all children of rootDomElement and renders <FatalErrorSc
|
|||
Array [
|
||||
Array [
|
||||
<KibanaRootContextProvider
|
||||
analytics={
|
||||
Object {
|
||||
"optIn": [MockFunction],
|
||||
"reportEvent": [MockFunction],
|
||||
"telemetryCounter$": Rx.Observable,
|
||||
}
|
||||
}
|
||||
globalStyles={true}
|
||||
i18n={
|
||||
Object {
|
||||
|
|
|
@ -13,6 +13,7 @@ expect.addSnapshotSerializer({
|
|||
print: () => `Rx.Observable`,
|
||||
});
|
||||
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { mockRender } from './fatal_errors_service.test.mocks';
|
||||
import { injectedMetadataServiceMock } from '@kbn/core-injected-metadata-browser-mocks';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
|
@ -22,6 +23,7 @@ import { FatalErrorsService } from './fatal_errors_service';
|
|||
function setupService() {
|
||||
const rootDomElement = document.createElement('div');
|
||||
|
||||
const analytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
const injectedMetadata = injectedMetadataServiceMock.createSetupContract();
|
||||
const theme = themeServiceMock.createSetupContract();
|
||||
|
||||
|
@ -39,7 +41,7 @@ function setupService() {
|
|||
rootDomElement,
|
||||
injectedMetadata,
|
||||
stopCoreSystem,
|
||||
fatalErrors: fatalErrorsService.setup({ injectedMetadata, i18n, theme }),
|
||||
fatalErrors: fatalErrorsService.setup({ analytics, injectedMetadata, i18n, theme }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import React from 'react';
|
|||
import { render } from 'react-dom';
|
||||
import { ReplaySubject, first, tap } from 'rxjs';
|
||||
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type { InternalInjectedMetadataSetup } from '@kbn/core-injected-metadata-browser-internal';
|
||||
import type { ThemeServiceSetup } from '@kbn/core-theme-browser';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
|
@ -20,6 +21,7 @@ import { getErrorInfo } from './get_error_info';
|
|||
|
||||
/** @internal */
|
||||
export interface FatalErrorsServiceSetupDeps {
|
||||
analytics: AnalyticsServiceStart;
|
||||
i18n: I18nStart;
|
||||
theme: ThemeServiceSetup;
|
||||
injectedMetadata: InternalInjectedMetadataSetup;
|
||||
|
@ -86,7 +88,7 @@ export class FatalErrorsService {
|
|||
return fatalErrors;
|
||||
}
|
||||
|
||||
private renderError({ i18n, theme, injectedMetadata }: FatalErrorsServiceSetupDeps) {
|
||||
private renderError({ analytics, i18n, theme, injectedMetadata }: FatalErrorsServiceSetupDeps) {
|
||||
// delete all content in the rootDomElement
|
||||
this.rootDomElement.textContent = '';
|
||||
|
||||
|
@ -95,7 +97,12 @@ export class FatalErrorsService {
|
|||
this.rootDomElement.appendChild(container);
|
||||
|
||||
render(
|
||||
<KibanaRootContextProvider i18n={i18n} theme={theme} globalStyles={true}>
|
||||
<KibanaRootContextProvider
|
||||
analytics={analytics}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
globalStyles={true}
|
||||
>
|
||||
<FatalErrorsScreen
|
||||
buildNumber={injectedMetadata.getKibanaBuildNumber()}
|
||||
kibanaVersion={injectedMetadata.getKibanaVersion()}
|
||||
|
|
|
@ -23,6 +23,8 @@
|
|||
"@kbn/test-subj-selector",
|
||||
"@kbn/test-jest-helpers",
|
||||
"@kbn/react-kibana-context-root",
|
||||
"@kbn/core-analytics-browser-mocks",
|
||||
"@kbn/core-analytics-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -79,6 +79,7 @@ export class NotificationsService {
|
|||
eventReporter,
|
||||
i18n: i18nDep,
|
||||
overlays,
|
||||
analytics,
|
||||
theme,
|
||||
targetDomElement: toastsContainer,
|
||||
}),
|
||||
|
@ -87,6 +88,7 @@ export class NotificationsService {
|
|||
title,
|
||||
error,
|
||||
openModal: overlays.openModal,
|
||||
analytics,
|
||||
i18n: i18nDep,
|
||||
theme,
|
||||
}),
|
||||
|
|
|
@ -4,6 +4,20 @@ exports[`#start() renders the GlobalToastList into the targetDomElement param 1`
|
|||
Array [
|
||||
Array [
|
||||
<KibanaRenderContextProvider
|
||||
analytics={
|
||||
Object {
|
||||
"optIn": [MockFunction],
|
||||
"reportEvent": [MockFunction],
|
||||
"telemetryCounter$": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
i18n={
|
||||
Object {
|
||||
"Context": [Function],
|
||||
|
|
|
@ -10,6 +10,7 @@ import { shallow } from 'enzyme';
|
|||
import React from 'react';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { ErrorToast } from './error_toast';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
|
||||
|
@ -21,6 +22,7 @@ interface ErrorToastProps {
|
|||
}
|
||||
|
||||
let openModal: jest.Mock;
|
||||
const mockAnalytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
const mockTheme = themeServiceMock.createStartContract();
|
||||
const mockI18n = i18nServiceMock.createStartContract();
|
||||
|
||||
|
@ -33,6 +35,7 @@ function render(props: ErrorToastProps = {}) {
|
|||
error={props.error || new Error('error message')}
|
||||
title={props.title || 'An error occured'}
|
||||
toastMessage={props.toastMessage || 'This is the toast message'}
|
||||
analytics={mockAnalytics}
|
||||
i18n={mockI18n}
|
||||
theme={mockTheme}
|
||||
/>
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { OverlayStart } from '@kbn/core-overlays-browser';
|
||||
import { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
|
@ -30,6 +31,7 @@ interface ErrorToastProps {
|
|||
error: Error;
|
||||
toastMessage: string;
|
||||
openModal: OverlayStart['openModal'];
|
||||
analytics: AnalyticsServiceStart;
|
||||
i18n: I18nStart;
|
||||
theme: ThemeServiceStart;
|
||||
}
|
||||
|
@ -55,9 +57,10 @@ export function showErrorDialog({
|
|||
title,
|
||||
error,
|
||||
openModal,
|
||||
analytics,
|
||||
i18n,
|
||||
theme,
|
||||
}: Pick<ErrorToastProps, 'error' | 'title' | 'openModal' | 'i18n' | 'theme'>) {
|
||||
}: Pick<ErrorToastProps, 'error' | 'title' | 'openModal' | 'analytics' | 'i18n' | 'theme'>) {
|
||||
let text = '';
|
||||
|
||||
if (isRequestError(error)) {
|
||||
|
@ -71,7 +74,7 @@ export function showErrorDialog({
|
|||
|
||||
const modal = openModal(
|
||||
mount(
|
||||
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
|
||||
<KibanaRenderContextProvider analytics={analytics} i18n={i18n} theme={theme}>
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{title}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
@ -104,6 +107,7 @@ export function ErrorToast({
|
|||
error,
|
||||
toastMessage,
|
||||
openModal,
|
||||
analytics,
|
||||
i18n,
|
||||
theme,
|
||||
}: ErrorToastProps) {
|
||||
|
@ -115,7 +119,7 @@ export function ErrorToast({
|
|||
size="s"
|
||||
color="danger"
|
||||
data-test-subj="errorToastBtn"
|
||||
onClick={() => showErrorDialog({ title, error, openModal, i18n, theme })}
|
||||
onClick={() => showErrorDialog({ title, error, openModal, analytics, i18n, theme })}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="core.toasts.errorToast.seeFullError"
|
||||
|
|
|
@ -10,6 +10,7 @@ import React from 'react';
|
|||
import * as Rx from 'rxjs';
|
||||
import { omitBy, isUndefined } from 'lodash';
|
||||
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
import type { OverlayStart } from '@kbn/core-overlays-browser';
|
||||
|
@ -44,6 +45,7 @@ export class ToastsApi implements IToasts {
|
|||
private uiSettings: IUiSettingsClient;
|
||||
|
||||
private overlays?: OverlayStart;
|
||||
private analytics?: AnalyticsServiceStart;
|
||||
private i18n?: I18nStart;
|
||||
private theme?: ThemeServiceStart;
|
||||
|
||||
|
@ -187,6 +189,7 @@ export class ToastsApi implements IToasts {
|
|||
error={error}
|
||||
title={options.title}
|
||||
toastMessage={message}
|
||||
analytics={this.analytics!}
|
||||
i18n={this.i18n!}
|
||||
theme={this.theme!}
|
||||
/>
|
||||
|
|
|
@ -47,6 +47,7 @@ describe('#start()', () => {
|
|||
expect(mockReactDomRender).not.toHaveBeenCalled();
|
||||
toasts.setup({ uiSettings: uiSettingsServiceMock.createSetupContract() });
|
||||
toasts.start({
|
||||
analytics: mockAnalytics,
|
||||
i18n: mockI18n,
|
||||
theme: mockTheme,
|
||||
targetDomElement,
|
||||
|
@ -65,6 +66,7 @@ describe('#start()', () => {
|
|||
).toBeInstanceOf(ToastsApi);
|
||||
expect(
|
||||
toasts.start({
|
||||
analytics: mockAnalytics,
|
||||
i18n: mockI18n,
|
||||
theme: mockTheme,
|
||||
targetDomElement,
|
||||
|
@ -83,6 +85,7 @@ describe('#stop()', () => {
|
|||
|
||||
toasts.setup({ uiSettings: uiSettingsServiceMock.createSetupContract() });
|
||||
toasts.start({
|
||||
analytics: mockAnalytics,
|
||||
i18n: mockI18n,
|
||||
theme: mockTheme,
|
||||
targetDomElement,
|
||||
|
@ -108,6 +111,7 @@ describe('#stop()', () => {
|
|||
|
||||
toasts.setup({ uiSettings: uiSettingsServiceMock.createSetupContract() });
|
||||
toasts.start({
|
||||
analytics: mockAnalytics,
|
||||
i18n: mockI18n,
|
||||
theme: mockTheme,
|
||||
targetDomElement,
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
|
@ -23,6 +24,7 @@ interface SetupDeps {
|
|||
}
|
||||
|
||||
interface StartDeps {
|
||||
analytics: AnalyticsServiceStart;
|
||||
i18n: I18nStart;
|
||||
overlays: OverlayStart;
|
||||
theme: ThemeServiceStart;
|
||||
|
@ -39,12 +41,12 @@ export class ToastsService {
|
|||
return this.api!;
|
||||
}
|
||||
|
||||
public start({ eventReporter, i18n, overlays, theme, targetDomElement }: StartDeps) {
|
||||
public start({ eventReporter, analytics, i18n, overlays, theme, targetDomElement }: StartDeps) {
|
||||
this.api!.start({ overlays, i18n, theme });
|
||||
this.targetDomElement = targetDomElement;
|
||||
|
||||
render(
|
||||
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
|
||||
<KibanaRenderContextProvider analytics={analytics} i18n={i18n} theme={theme}>
|
||||
<GlobalToastList
|
||||
dismissToast={(toastId: string) => this.api!.remove(toastId)}
|
||||
toasts$={this.api!.get$()}
|
||||
|
|
|
@ -12,6 +12,20 @@ exports[`FlyoutService openFlyout() renders a flyout to the DOM 1`] = `
|
|||
Array [
|
||||
Array [
|
||||
<KibanaRenderContextProvider
|
||||
analytics={
|
||||
Object {
|
||||
"optIn": [MockFunction],
|
||||
"reportEvent": [MockFunction],
|
||||
"telemetryCounter$": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
i18n={
|
||||
Object {
|
||||
"Context": [MockFunction],
|
||||
|
@ -45,13 +59,42 @@ exports[`FlyoutService openFlyout() with a currently active flyout replaces the
|
|||
Array [
|
||||
Array [
|
||||
<KibanaRenderContextProvider
|
||||
analytics={
|
||||
Object {
|
||||
"optIn": [MockFunction],
|
||||
"reportEvent": [MockFunction],
|
||||
"telemetryCounter$": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
i18n={
|
||||
Object {
|
||||
"Context": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"children": <KibanaErrorBoundaryProvider>
|
||||
"children": <KibanaErrorBoundaryProvider
|
||||
analytics={
|
||||
Object {
|
||||
"optIn": [MockFunction],
|
||||
"reportEvent": [MockFunction],
|
||||
"telemetryCounter$": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<KibanaErrorBoundary>
|
||||
<EuiFlyout
|
||||
onClose={[Function]}
|
||||
|
@ -70,7 +113,22 @@ Array [
|
|||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": <KibanaErrorBoundaryProvider>
|
||||
"value": <KibanaErrorBoundaryProvider
|
||||
analytics={
|
||||
Object {
|
||||
"optIn": [MockFunction],
|
||||
"reportEvent": [MockFunction],
|
||||
"telemetryCounter$": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<KibanaErrorBoundary>
|
||||
<EuiFlyout
|
||||
onClose={[Function]}
|
||||
|
@ -108,13 +166,42 @@ Array [
|
|||
],
|
||||
Array [
|
||||
<KibanaRenderContextProvider
|
||||
analytics={
|
||||
Object {
|
||||
"optIn": [MockFunction],
|
||||
"reportEvent": [MockFunction],
|
||||
"telemetryCounter$": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
i18n={
|
||||
Object {
|
||||
"Context": [MockFunction] {
|
||||
"calls": Array [
|
||||
Array [
|
||||
Object {
|
||||
"children": <KibanaErrorBoundaryProvider>
|
||||
"children": <KibanaErrorBoundaryProvider
|
||||
analytics={
|
||||
Object {
|
||||
"optIn": [MockFunction],
|
||||
"reportEvent": [MockFunction],
|
||||
"telemetryCounter$": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<KibanaErrorBoundary>
|
||||
<EuiFlyout
|
||||
onClose={[Function]}
|
||||
|
@ -133,7 +220,22 @@ Array [
|
|||
"results": Array [
|
||||
Object {
|
||||
"type": "return",
|
||||
"value": <KibanaErrorBoundaryProvider>
|
||||
"value": <KibanaErrorBoundaryProvider
|
||||
analytics={
|
||||
Object {
|
||||
"optIn": [MockFunction],
|
||||
"reportEvent": [MockFunction],
|
||||
"telemetryCounter$": Subject {
|
||||
"closed": false,
|
||||
"currentObservers": null,
|
||||
"hasError": false,
|
||||
"isStopped": false,
|
||||
"observers": Array [],
|
||||
"thrownError": null,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<KibanaErrorBoundary>
|
||||
<EuiFlyout
|
||||
onClose={[Function]}
|
||||
|
|
|
@ -9,12 +9,14 @@
|
|||
import { mockReactDomRender, mockReactDomUnmount } from '../overlay.test.mocks';
|
||||
|
||||
import { mount } from 'enzyme';
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { FlyoutService } from './flyout_service';
|
||||
import type { OverlayRef } from '@kbn/core-mount-utils-browser';
|
||||
import type { OverlayFlyoutStart } from '@kbn/core-overlays-browser';
|
||||
|
||||
const analyticsMock = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
const i18nMock = i18nServiceMock.createStartContract();
|
||||
const themeMock = themeServiceMock.createStartContract();
|
||||
|
||||
|
@ -33,6 +35,7 @@ const mountText = (text: string) => (container: HTMLElement) => {
|
|||
const getServiceStart = () => {
|
||||
const service = new FlyoutService();
|
||||
return service.start({
|
||||
analytics: analyticsMock,
|
||||
i18n: i18nMock,
|
||||
theme: themeMock,
|
||||
targetDomElement: document.createElement('div'),
|
||||
|
|
|
@ -12,6 +12,7 @@ import { EuiFlyout } from '@elastic/eui';
|
|||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Subject } from 'rxjs';
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { MountPoint, OverlayRef } from '@kbn/core-mount-utils-browser';
|
||||
|
@ -60,6 +61,7 @@ class FlyoutRef implements OverlayRef {
|
|||
}
|
||||
|
||||
interface StartDeps {
|
||||
analytics: AnalyticsServiceStart;
|
||||
i18n: I18nStart;
|
||||
theme: ThemeServiceStart;
|
||||
targetDomElement: Element;
|
||||
|
@ -70,7 +72,7 @@ export class FlyoutService {
|
|||
private activeFlyout: FlyoutRef | null = null;
|
||||
private targetDomElement: Element | null = null;
|
||||
|
||||
public start({ i18n, theme, targetDomElement }: StartDeps): OverlayFlyoutStart {
|
||||
public start({ analytics, i18n, theme, targetDomElement }: StartDeps): OverlayFlyoutStart {
|
||||
this.targetDomElement = targetDomElement;
|
||||
|
||||
return {
|
||||
|
@ -101,7 +103,7 @@ export class FlyoutService {
|
|||
};
|
||||
|
||||
render(
|
||||
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
|
||||
<KibanaRenderContextProvider analytics={analytics} i18n={i18n} theme={theme}>
|
||||
<EuiFlyout {...options} onClose={onCloseFlyout}>
|
||||
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
|
||||
</EuiFlyout>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -10,6 +10,7 @@ import { mockReactDomRender, mockReactDomUnmount } from '../overlay.test.mocks';
|
|||
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { ModalService } from './modal_service';
|
||||
|
@ -17,6 +18,7 @@ import type { OverlayModalStart } from '@kbn/core-overlays-browser';
|
|||
import { mountReactNode } from '@kbn/core-mount-utils-browser-internal';
|
||||
import type { OverlayRef } from '@kbn/core-mount-utils-browser';
|
||||
|
||||
const analyticsMock = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
const i18nMock = i18nServiceMock.createStartContract();
|
||||
const themeMock = themeServiceMock.createStartContract();
|
||||
|
||||
|
@ -28,6 +30,7 @@ beforeEach(() => {
|
|||
const getServiceStart = () => {
|
||||
const service = new ModalService();
|
||||
return service.start({
|
||||
analytics: analyticsMock,
|
||||
i18n: i18nMock,
|
||||
theme: themeMock,
|
||||
targetDomElement: document.createElement('div'),
|
||||
|
|
|
@ -13,6 +13,7 @@ import { EuiModal, EuiConfirmModal } from '@elastic/eui';
|
|||
import React from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import { Subject } from 'rxjs';
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { MountPoint, OverlayRef } from '@kbn/core-mount-utils-browser';
|
||||
|
@ -56,6 +57,7 @@ class ModalRef implements OverlayRef {
|
|||
interface StartDeps {
|
||||
i18n: I18nStart;
|
||||
theme: ThemeServiceStart;
|
||||
analytics: AnalyticsServiceStart;
|
||||
targetDomElement: Element;
|
||||
}
|
||||
|
||||
|
@ -64,7 +66,7 @@ export class ModalService {
|
|||
private activeModal: ModalRef | null = null;
|
||||
private targetDomElement: Element | null = null;
|
||||
|
||||
public start({ i18n, theme, targetDomElement }: StartDeps): OverlayModalStart {
|
||||
public start({ analytics, i18n, theme, targetDomElement }: StartDeps): OverlayModalStart {
|
||||
this.targetDomElement = targetDomElement;
|
||||
|
||||
return {
|
||||
|
@ -87,7 +89,7 @@ export class ModalService {
|
|||
this.activeModal = modal;
|
||||
|
||||
render(
|
||||
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
|
||||
<KibanaRenderContextProvider analytics={analytics} i18n={i18n} theme={theme}>
|
||||
<EuiModal {...options} onClose={() => modal.close()}>
|
||||
<MountWrapper mount={mount} className="kbnOverlayMountWrapper" />
|
||||
</EuiModal>
|
||||
|
@ -147,7 +149,7 @@ export class ModalService {
|
|||
};
|
||||
|
||||
render(
|
||||
<KibanaRenderContextProvider i18n={i18n} theme={theme}>
|
||||
<KibanaRenderContextProvider analytics={analytics} i18n={i18n} theme={theme}>
|
||||
<EuiConfirmModal {...props} />
|
||||
</KibanaRenderContextProvider>,
|
||||
targetDomElement
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
|
||||
|
@ -15,6 +16,7 @@ import { FlyoutService } from './flyout';
|
|||
import { ModalService } from './modal';
|
||||
|
||||
interface StartDeps {
|
||||
analytics: AnalyticsServiceStart;
|
||||
i18n: I18nStart;
|
||||
theme: ThemeServiceStart;
|
||||
targetDomElement: HTMLElement;
|
||||
|
@ -27,16 +29,26 @@ export class OverlayService {
|
|||
private modalService = new ModalService();
|
||||
private flyoutService = new FlyoutService();
|
||||
|
||||
public start({ i18n, targetDomElement, uiSettings, theme }: StartDeps): OverlayStart {
|
||||
public start({ analytics, i18n, targetDomElement, uiSettings, theme }: StartDeps): OverlayStart {
|
||||
const flyoutElement = document.createElement('div');
|
||||
targetDomElement.appendChild(flyoutElement);
|
||||
const flyouts = this.flyoutService.start({ i18n, theme, targetDomElement: flyoutElement });
|
||||
const flyouts = this.flyoutService.start({
|
||||
analytics,
|
||||
i18n,
|
||||
theme,
|
||||
targetDomElement: flyoutElement,
|
||||
});
|
||||
|
||||
const banners = this.bannersService.start({ i18n, uiSettings });
|
||||
|
||||
const modalElement = document.createElement('div');
|
||||
targetDomElement.appendChild(modalElement);
|
||||
const modals = this.modalService.start({ i18n, theme, targetDomElement: modalElement });
|
||||
const modals = this.modalService.start({
|
||||
analytics,
|
||||
i18n,
|
||||
theme,
|
||||
targetDomElement: modalElement,
|
||||
});
|
||||
|
||||
return {
|
||||
banners,
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
"@kbn/core-theme-browser-mocks",
|
||||
"@kbn/i18n",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/core-analytics-browser-mocks",
|
||||
"@kbn/core-analytics-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -10,6 +10,7 @@ import React from 'react';
|
|||
import { act } from 'react-dom/test-utils';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { applicationServiceMock } from '@kbn/core-application-browser-mocks';
|
||||
import { chromeServiceMock } from '@kbn/core-chrome-browser-mocks';
|
||||
import { overlayServiceMock } from '@kbn/core-overlays-browser-mocks';
|
||||
|
@ -18,6 +19,7 @@ import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
|
|||
import { RenderingService } from './rendering_service';
|
||||
|
||||
describe('RenderingService#start', () => {
|
||||
let analytics: ReturnType<typeof analyticsServiceMock.createAnalyticsServiceStart>;
|
||||
let application: ReturnType<typeof applicationServiceMock.createInternalStartContract>;
|
||||
let chrome: ReturnType<typeof chromeServiceMock.createStartContract>;
|
||||
let overlays: ReturnType<typeof overlayServiceMock.createStartContract>;
|
||||
|
@ -27,6 +29,8 @@ describe('RenderingService#start', () => {
|
|||
let rendering: RenderingService;
|
||||
|
||||
beforeEach(() => {
|
||||
analytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
|
||||
application = applicationServiceMock.createInternalStartContract();
|
||||
application.getComponent.mockReturnValue(<div>Hello application!</div>);
|
||||
|
||||
|
@ -47,6 +51,7 @@ describe('RenderingService#start', () => {
|
|||
|
||||
const startService = () => {
|
||||
return rendering.start({
|
||||
analytics,
|
||||
application,
|
||||
chrome,
|
||||
overlays,
|
||||
|
|
|
@ -10,15 +10,17 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom';
|
||||
import { pairwise, startWith } from 'rxjs/operators';
|
||||
|
||||
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
|
||||
import type { OverlayStart } from '@kbn/core-overlays-browser';
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type { InternalApplicationStart } from '@kbn/core-application-browser-internal';
|
||||
import type { InternalChromeStart } from '@kbn/core-chrome-browser-internal';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { OverlayStart } from '@kbn/core-overlays-browser';
|
||||
import type { ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
|
||||
import { AppWrapper } from './app_containers';
|
||||
|
||||
export interface StartDeps {
|
||||
analytics: AnalyticsServiceStart;
|
||||
application: InternalApplicationStart;
|
||||
chrome: InternalChromeStart;
|
||||
overlays: OverlayStart;
|
||||
|
@ -36,7 +38,7 @@ export interface StartDeps {
|
|||
* @internal
|
||||
*/
|
||||
export class RenderingService {
|
||||
start({ application, chrome, overlays, theme, i18n, targetDomElement }: StartDeps) {
|
||||
start({ analytics, application, chrome, overlays, theme, i18n, targetDomElement }: StartDeps) {
|
||||
const chromeHeader = chrome.getHeaderComponent();
|
||||
const appComponent = application.getComponent();
|
||||
const bannerComponent = overlays.banners.getComponent();
|
||||
|
@ -51,7 +53,12 @@ export class RenderingService {
|
|||
});
|
||||
|
||||
ReactDOM.render(
|
||||
<KibanaRootContextProvider i18n={i18n} theme={theme} globalStyles={true}>
|
||||
<KibanaRootContextProvider
|
||||
analytics={analytics}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
globalStyles={true}
|
||||
>
|
||||
<>
|
||||
{/* Fixed headers */}
|
||||
{chromeHeader}
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
"@kbn/core-theme-browser-mocks",
|
||||
"@kbn/core-i18n-browser-mocks",
|
||||
"@kbn/react-kibana-context-root",
|
||||
"@kbn/core-analytics-browser",
|
||||
"@kbn/core-analytics-browser-mocks",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -478,6 +478,7 @@ describe('#start()', () => {
|
|||
await startCore();
|
||||
expect(MockRenderingService.start).toHaveBeenCalledTimes(1);
|
||||
expect(MockRenderingService.start).toHaveBeenCalledWith({
|
||||
analytics: expect.any(Object),
|
||||
application: expect.any(Object),
|
||||
chrome: expect.any(Object),
|
||||
overlays: expect.any(Object),
|
||||
|
|
|
@ -216,18 +216,18 @@ export class CoreSystem {
|
|||
// Setup FatalErrorsService and it's dependencies first so that we're
|
||||
// able to render any errors.
|
||||
const injectedMetadata = this.injectedMetadata.setup();
|
||||
const analytics = this.analytics.setup({ injectedMetadata });
|
||||
const theme = this.theme.setup({ injectedMetadata });
|
||||
|
||||
this.fatalErrorsSetup = this.fatalErrors.setup({
|
||||
injectedMetadata,
|
||||
analytics,
|
||||
theme,
|
||||
i18n: this.i18n.getContext(),
|
||||
});
|
||||
await this.integrations.setup();
|
||||
this.docLinks.setup();
|
||||
|
||||
const analytics = this.analytics.setup({ injectedMetadata });
|
||||
|
||||
this.registerLoadedKibanaEventType(analytics);
|
||||
|
||||
const executionContext = this.executionContext.setup({ analytics });
|
||||
|
@ -300,11 +300,12 @@ export class CoreSystem {
|
|||
|
||||
const overlays = this.overlay.start({
|
||||
i18n,
|
||||
analytics,
|
||||
theme,
|
||||
uiSettings,
|
||||
targetDomElement: overlayTargetDomElement,
|
||||
});
|
||||
const notifications = await this.notifications.start({
|
||||
const notifications = this.notifications.start({
|
||||
analytics,
|
||||
i18n,
|
||||
overlays,
|
||||
|
@ -312,7 +313,13 @@ export class CoreSystem {
|
|||
targetDomElement: notificationsTargetDomElement,
|
||||
});
|
||||
const customBranding = this.customBranding.start();
|
||||
const application = await this.application.start({ http, theme, overlays, customBranding });
|
||||
const application = await this.application.start({
|
||||
http,
|
||||
theme,
|
||||
overlays,
|
||||
customBranding,
|
||||
analytics,
|
||||
});
|
||||
|
||||
const executionContext = this.executionContext.start({
|
||||
curApp$: application.currentAppId$,
|
||||
|
@ -362,6 +369,7 @@ export class CoreSystem {
|
|||
this.rendering.start({
|
||||
application,
|
||||
chrome,
|
||||
analytics,
|
||||
i18n,
|
||||
overlays,
|
||||
theme,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { I18nProvider } from '@kbn/i18n-react';
|
|||
|
||||
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { I18nStart } from '@kbn/core-i18n-browser';
|
||||
|
||||
import { createFormServicesMock } from '@kbn/management-settings-components-form/mocks';
|
||||
|
@ -19,11 +20,13 @@ import { getSettingsMock } from '@kbn/management-settings-utilities/mocks/settin
|
|||
import { SettingsApplicationProvider, SettingsApplicationServices } from '../services';
|
||||
|
||||
const createRootMock = () => {
|
||||
const analytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
const i18n: I18nStart = {
|
||||
Context: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
};
|
||||
const theme = themeServiceMock.createStartContract();
|
||||
return {
|
||||
analytics,
|
||||
i18n,
|
||||
theme,
|
||||
};
|
||||
|
|
|
@ -30,5 +30,6 @@
|
|||
"@kbn/react-kibana-context-root",
|
||||
"@kbn/core-theme-browser-mocks",
|
||||
"@kbn/core-i18n-browser",
|
||||
"@kbn/core-analytics-browser-mocks",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -11,17 +11,20 @@ import { I18nProvider } from '@kbn/i18n-react';
|
|||
|
||||
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { I18nStart } from '@kbn/core-i18n-browser';
|
||||
|
||||
import { FieldInputProvider } from '../services';
|
||||
import { FieldInputServices } from '../types';
|
||||
|
||||
const createRootMock = () => {
|
||||
const analytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
const i18n: I18nStart = {
|
||||
Context: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
};
|
||||
const theme = themeServiceMock.createStartContract();
|
||||
return {
|
||||
analytics,
|
||||
i18n,
|
||||
theme,
|
||||
};
|
||||
|
|
|
@ -28,5 +28,6 @@
|
|||
"@kbn/react-kibana-context-root",
|
||||
"@kbn/core-theme-browser-mocks",
|
||||
"@kbn/core-i18n-browser",
|
||||
"@kbn/core-analytics-browser-mocks",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { I18nProvider } from '@kbn/i18n-react';
|
|||
|
||||
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { I18nStart } from '@kbn/core-i18n-browser';
|
||||
|
||||
import { createFieldInputServicesMock } from '@kbn/management-settings-components-field-input/mocks';
|
||||
|
@ -19,11 +20,13 @@ import { FieldRowProvider } from '../services';
|
|||
import { FieldRowServices } from '../types';
|
||||
|
||||
const createRootMock = () => {
|
||||
const analytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
const i18n: I18nStart = {
|
||||
Context: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
};
|
||||
const theme = themeServiceMock.createStartContract();
|
||||
return {
|
||||
analytics,
|
||||
i18n,
|
||||
theme,
|
||||
};
|
||||
|
|
|
@ -29,5 +29,6 @@
|
|||
"@kbn/react-kibana-context-root",
|
||||
"@kbn/core-theme-browser-mocks",
|
||||
"@kbn/core-i18n-browser",
|
||||
"@kbn/core-analytics-browser-mocks",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { I18nProvider } from '@kbn/i18n-react';
|
|||
|
||||
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
|
||||
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { I18nStart } from '@kbn/core-i18n-browser';
|
||||
|
||||
import { createFieldRowServicesMock } from '@kbn/management-settings-components-field-row/mocks';
|
||||
|
@ -18,11 +19,13 @@ import { FormProvider } from '../services';
|
|||
import type { FormServices } from '../types';
|
||||
|
||||
const createRootMock = () => {
|
||||
const analytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
const i18n: I18nStart = {
|
||||
Context: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
};
|
||||
const theme = themeServiceMock.createStartContract();
|
||||
return {
|
||||
analytics,
|
||||
i18n,
|
||||
theme,
|
||||
};
|
||||
|
|
|
@ -32,5 +32,6 @@
|
|||
"@kbn/management-settings-components-field-input",
|
||||
"@kbn/management-settings-components-field-category",
|
||||
"@kbn/management-settings-utilities",
|
||||
"@kbn/core-analytics-browser-mocks",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
*/
|
||||
|
||||
export { Fallback } from './src/fallback';
|
||||
export { withSuspense } from './src/with_suspense';
|
||||
export { getClosestLink, hasActiveModifierKey } from './src/utils';
|
||||
export { withSuspense, type WithSuspenseExtendedDeps } from './src/with_suspense';
|
||||
|
|
|
@ -6,13 +6,25 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { Suspense, ComponentType, ReactElement, Ref } from 'react';
|
||||
import { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
|
||||
import React, { ComponentType, ReactElement, Ref, Suspense } from 'react';
|
||||
|
||||
import { Fallback } from './fallback';
|
||||
|
||||
/**
|
||||
* A HOC which supplies React.Suspense with a fallback component, and a `EuiErrorBoundary` to contain errors.
|
||||
* Optional services that the Suspense wrapper can use
|
||||
* @public
|
||||
*/
|
||||
export interface WithSuspenseExtendedDeps {
|
||||
/**
|
||||
* The `AnalyticsServiceStart` object from `CoreStart`
|
||||
*/
|
||||
analytics?: AnalyticsServiceStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* A HOC which supplies React.Suspense with a fallback component, and a `KibanaErrorBoundary` to contain errors.
|
||||
* @param Component A component deferred by `React.lazy`
|
||||
* @param fallback A fallback component to render while things load; default is `Fallback` from SharedUX.
|
||||
*/
|
||||
|
@ -20,8 +32,8 @@ export const withSuspense = <P extends {}, R = {}>(
|
|||
Component: ComponentType<P>,
|
||||
fallback: ReactElement | null = <Fallback />
|
||||
) =>
|
||||
React.forwardRef((props: P, ref: Ref<R>) => (
|
||||
<KibanaErrorBoundaryProvider>
|
||||
React.forwardRef((props: P & WithSuspenseExtendedDeps, ref: Ref<R>) => (
|
||||
<KibanaErrorBoundaryProvider analytics={props.analytics}>
|
||||
<KibanaErrorBoundary>
|
||||
<Suspense fallback={fallback}>
|
||||
<Component {...props} ref={ref} />
|
||||
|
|
|
@ -17,5 +17,6 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/shared-ux-error-boundary",
|
||||
"@kbn/core-analytics-browser",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -6,7 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import React, { useEffect } from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import type { DecoratorFn } from '@storybook/react';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
|
||||
|
@ -14,6 +16,7 @@ import 'core_styles';
|
|||
import { BehaviorSubject } from 'rxjs';
|
||||
import { CoreTheme } from '@kbn/core-theme-browser';
|
||||
import { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
|
||||
|
||||
const theme$ = new BehaviorSubject<CoreTheme>({ darkMode: false });
|
||||
|
@ -22,6 +25,12 @@ const i18n: I18nStart = {
|
|||
Context: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
};
|
||||
|
||||
const analytics: AnalyticsServiceStart = {
|
||||
reportEvent: action('Report telemetry event'),
|
||||
optIn: action('Opt in'),
|
||||
telemetryCounter$: new Subject(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Storybook decorator using the `KibanaContextProvider`. Uses the value from
|
||||
* `globals` provided by the Storybook theme switcher to set the `colorMode`.
|
||||
|
@ -34,7 +43,7 @@ const KibanaContextDecorator: DecoratorFn = (storyFn, { globals }) => {
|
|||
}, [colorMode]);
|
||||
|
||||
return (
|
||||
<KibanaRootContextProvider {...{ theme: { theme$ }, i18n }}>
|
||||
<KibanaRootContextProvider {...{ theme: { theme$ }, analytics, i18n }}>
|
||||
{storyFn()}
|
||||
</KibanaRootContextProvider>
|
||||
);
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"@kbn/i18n-react",
|
||||
"@kbn/core-i18n-browser",
|
||||
"@kbn/react-kibana-context-root",
|
||||
"@kbn/core-analytics-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -27,7 +27,7 @@ export const KibanaRenderContextProvider: FC<KibanaRenderContextProviderProps> =
|
|||
}) => {
|
||||
return (
|
||||
<KibanaRootContextProvider globalStyles={false} {...props}>
|
||||
<KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundaryProvider analytics={props.analytics}>
|
||||
<KibanaErrorBoundary>{children}</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryProvider>
|
||||
</KibanaRootContextProvider>
|
||||
|
|
|
@ -19,6 +19,7 @@ import { BehaviorSubject } from 'rxjs';
|
|||
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import type { DecoratorFn } from '@storybook/react';
|
||||
import type { CoreTheme } from '@kbn/core-theme-browser';
|
||||
|
@ -26,6 +27,10 @@ import type { I18nStart } from '@kbn/core-i18n-browser';
|
|||
|
||||
const theme$ = new BehaviorSubject<CoreTheme>({ darkMode: false });
|
||||
|
||||
const analytics = {
|
||||
reportEvent: action('telemetry-report-event'),
|
||||
};
|
||||
|
||||
const i18n: I18nStart = {
|
||||
Context: ({ children }) => <I18nProvider>{children}</I18nProvider>,
|
||||
};
|
||||
|
@ -38,7 +43,7 @@ export const KibanaContextDecorator: DecoratorFn = (storyFn, { globals }) => {
|
|||
}, [colorMode]);
|
||||
|
||||
return (
|
||||
<KibanaRootContextProvider {...{ theme: { theme$ }, i18n }}>
|
||||
<KibanaRootContextProvider {...{ theme: { theme$ }, analytics, i18n }}>
|
||||
{storyFn()}
|
||||
</KibanaRootContextProvider>
|
||||
);
|
||||
|
|
|
@ -14,6 +14,8 @@ import { useEuiTheme } from '@elastic/eui';
|
|||
import type { UseEuiTheme } from '@elastic/eui';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import type { KibanaTheme } from '@kbn/react-kibana-context-common';
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
|
||||
import { KibanaRootContextProvider } from './root_provider';
|
||||
import { I18nStart } from '@kbn/core-i18n-browser';
|
||||
|
@ -21,9 +23,11 @@ import { I18nStart } from '@kbn/core-i18n-browser';
|
|||
describe('KibanaRootContextProvider', () => {
|
||||
let euiTheme: UseEuiTheme | undefined;
|
||||
let i18nMock: I18nStart;
|
||||
let analytics: AnalyticsServiceStart;
|
||||
|
||||
beforeEach(() => {
|
||||
euiTheme = undefined;
|
||||
analytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
i18nMock = i18nServiceMock.createStartContract();
|
||||
});
|
||||
|
||||
|
@ -56,7 +60,11 @@ describe('KibanaRootContextProvider', () => {
|
|||
const coreTheme: KibanaTheme = { darkMode: true };
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaRootContextProvider i18n={i18nMock} theme={{ theme$: of(coreTheme) }}>
|
||||
<KibanaRootContextProvider
|
||||
analytics={analytics}
|
||||
i18n={i18nMock}
|
||||
theme={{ theme$: of(coreTheme) }}
|
||||
>
|
||||
<InnerComponent />
|
||||
</KibanaRootContextProvider>
|
||||
);
|
||||
|
@ -70,7 +78,11 @@ describe('KibanaRootContextProvider', () => {
|
|||
const coreTheme$ = new BehaviorSubject<KibanaTheme>({ darkMode: true });
|
||||
|
||||
const wrapper = mountWithIntl(
|
||||
<KibanaRootContextProvider i18n={i18nMock} theme={{ theme$: coreTheme$ }}>
|
||||
<KibanaRootContextProvider
|
||||
analytics={analytics}
|
||||
i18n={i18nMock}
|
||||
theme={{ theme$: coreTheme$ }}
|
||||
>
|
||||
<InnerComponent />
|
||||
</KibanaRootContextProvider>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { KibanaEuiProvider, type KibanaEuiProviderProps } from './eui_provider';
|
||||
|
@ -15,6 +16,8 @@ import { KibanaEuiProvider, type KibanaEuiProviderProps } from './eui_provider';
|
|||
export interface KibanaRootContextProviderProps extends KibanaEuiProviderProps {
|
||||
/** The `I18nStart` API from `CoreStart`. */
|
||||
i18n: I18nStart;
|
||||
/** The `AnalyticsServiceStart` API from `CoreStart`. */
|
||||
analytics?: AnalyticsServiceStart;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,5 +21,7 @@
|
|||
"@kbn/core-i18n-browser-mocks",
|
||||
"@kbn/core-i18n-browser",
|
||||
"@kbn/core-base-common",
|
||||
"@kbn/core-analytics-browser",
|
||||
"@kbn/core-analytics-browser-mocks",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -13,11 +13,13 @@ import { useEuiTheme } from '@elastic/eui';
|
|||
import type { UseEuiTheme } from '@elastic/eui';
|
||||
import type { CoreTheme } from '@kbn/core/public';
|
||||
import { toMountPoint } from './to_mount_point';
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
|
||||
|
||||
describe('toMountPoint', () => {
|
||||
let euiTheme: UseEuiTheme;
|
||||
const i18n = i18nServiceMock.createStartContract();
|
||||
const analytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
|
||||
const InnerComponent: FC = () => {
|
||||
const theme = useEuiTheme();
|
||||
|
@ -39,7 +41,7 @@ describe('toMountPoint', () => {
|
|||
|
||||
it('exposes the euiTheme when `theme$` is provided', async () => {
|
||||
const theme = { theme$: of<CoreTheme>({ darkMode: true }) };
|
||||
const mount = toMountPoint(<InnerComponent />, { theme, i18n });
|
||||
const mount = toMountPoint(<InnerComponent />, { theme, i18n, analytics });
|
||||
|
||||
const targetEl = document.createElement('div');
|
||||
mount(targetEl);
|
||||
|
@ -52,7 +54,7 @@ describe('toMountPoint', () => {
|
|||
it('propagates changes of the theme$ observable', async () => {
|
||||
const theme$ = new BehaviorSubject<CoreTheme>({ darkMode: true });
|
||||
|
||||
const mount = toMountPoint(<InnerComponent />, { theme: { theme$ }, i18n });
|
||||
const mount = toMountPoint(<InnerComponent />, { theme: { theme$ }, i18n, analytics });
|
||||
|
||||
const targetEl = document.createElement('div');
|
||||
mount(targetEl);
|
||||
|
|
|
@ -14,7 +14,10 @@ import {
|
|||
KibanaRenderContextProviderProps,
|
||||
} from '@kbn/react-kibana-context-render';
|
||||
|
||||
export type ToMountPointParams = Pick<KibanaRenderContextProviderProps, 'i18n' | 'theme'>;
|
||||
export type ToMountPointParams = Pick<
|
||||
KibanaRenderContextProviderProps,
|
||||
'analytics' | 'i18n' | 'theme'
|
||||
>;
|
||||
|
||||
/**
|
||||
* MountPoint converter for react nodes.
|
||||
|
|
|
@ -20,5 +20,6 @@
|
|||
"@kbn/i18n",
|
||||
"@kbn/core-i18n-browser-mocks",
|
||||
"@kbn/react-kibana-context-render",
|
||||
"@kbn/core-analytics-browser-mocks",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export { KibanaErrorBoundary } from './src/ui/error_boundary';
|
||||
export { KibanaErrorBoundaryProvider } from './src/services/error_boundary_services';
|
||||
export { KibanaErrorBoundary } from './src/ui/error_boundary';
|
||||
export { ThrowIfError } from './src/ui/throw_if_error';
|
||||
|
||||
export { REACT_FATAL_ERROR_EVENT_TYPE, reactFatalErrorSchema } from './lib/telemetry_events';
|
||||
export type { ReactFatalError } from './lib/telemetry_events';
|
||||
|
|
34
packages/shared-ux/error_boundary/lib/telemetry_events.ts
Normal file
34
packages/shared-ux/error_boundary/lib/telemetry_events.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
/** @internal */
|
||||
export const REACT_FATAL_ERROR_EVENT_TYPE = 'fatal-error-react';
|
||||
|
||||
/** @internal */
|
||||
export interface ReactFatalError {
|
||||
component_name: string;
|
||||
error_message: string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export const reactFatalErrorSchema = {
|
||||
component_name: {
|
||||
type: 'keyword' as const,
|
||||
_meta: {
|
||||
description: 'Name of react component that threw an error',
|
||||
optional: false as const,
|
||||
},
|
||||
},
|
||||
error_message: {
|
||||
type: 'keyword' as const,
|
||||
_meta: {
|
||||
description: 'Message from the error',
|
||||
optional: false as const,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -10,8 +10,11 @@ import { KibanaErrorService } from '../../src/services/error_service';
|
|||
import { KibanaErrorBoundaryServices } from '../../types';
|
||||
|
||||
export const getServicesMock = (): KibanaErrorBoundaryServices => {
|
||||
const mockDeps = {
|
||||
analytics: { reportEvent: jest.fn() },
|
||||
};
|
||||
return {
|
||||
onClickRefresh: jest.fn().mockResolvedValue(undefined),
|
||||
errorService: new KibanaErrorService(),
|
||||
errorService: new KibanaErrorService(mockDeps),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -25,15 +25,13 @@ export class KibanaErrorBoundaryStorybookMock extends AbstractStorybookMock<
|
|||
dependencies = [];
|
||||
|
||||
getServices(params: Params = {}): KibanaErrorBoundaryServices {
|
||||
const reloadWindowAction = action('Reload window');
|
||||
const onClickRefresh = () => {
|
||||
reloadWindowAction();
|
||||
};
|
||||
const onClickRefresh = action('Reload window');
|
||||
const analytics = { reportEvent: action('Report telemetry event') };
|
||||
|
||||
return {
|
||||
...params,
|
||||
onClickRefresh,
|
||||
errorService: new KibanaErrorService(),
|
||||
errorService: new KibanaErrorService({ analytics }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
|
||||
import { KibanaErrorBoundaryProviderDeps } from '../../types';
|
||||
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '../..';
|
||||
import { BadComponent } from '../../mocks';
|
||||
|
||||
describe('<KibanaErrorBoundaryProvider>', () => {
|
||||
let analytics: KibanaErrorBoundaryProviderDeps['analytics'];
|
||||
beforeEach(() => {
|
||||
analytics = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
});
|
||||
|
||||
it('creates a context of services for KibanaErrorBoundary', async () => {
|
||||
const reportEventSpy = jest.spyOn(analytics!, 'reportEvent');
|
||||
|
||||
const { findByTestId } = render(
|
||||
<KibanaErrorBoundaryProvider analytics={analytics}>
|
||||
<KibanaErrorBoundary>
|
||||
<BadComponent />
|
||||
</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryProvider>
|
||||
);
|
||||
(await findByTestId('clickForErrorBtn')).click();
|
||||
|
||||
expect(reportEventSpy).toBeCalledWith('fatal-error-react', {
|
||||
component_name: 'BadComponent',
|
||||
error_message: 'Error: This is an error to show the test user!',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses higher-level context if available', async () => {
|
||||
const reportEventSpy1 = jest.spyOn(analytics!, 'reportEvent');
|
||||
|
||||
const analytics2 = analyticsServiceMock.createAnalyticsServiceStart();
|
||||
const reportEventSpy2 = jest.spyOn(analytics2, 'reportEvent');
|
||||
|
||||
const { findByTestId } = render(
|
||||
<KibanaErrorBoundaryProvider analytics={analytics}>
|
||||
<KibanaErrorBoundary>
|
||||
Hello world
|
||||
<KibanaErrorBoundaryProvider analytics={analytics2}>
|
||||
<KibanaErrorBoundary>
|
||||
<BadComponent />
|
||||
</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryProvider>
|
||||
</KibanaErrorBoundary>
|
||||
</KibanaErrorBoundaryProvider>
|
||||
);
|
||||
(await findByTestId('clickForErrorBtn')).click();
|
||||
|
||||
expect(reportEventSpy2).not.toBeCalled();
|
||||
expect(reportEventSpy1).toBeCalledWith('fatal-error-react', {
|
||||
component_name: 'BadComponent',
|
||||
error_message: 'Error: This is an error to show the test user!',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,39 +8,49 @@
|
|||
|
||||
import React, { FC, useContext, useMemo } from 'react';
|
||||
|
||||
import { KibanaErrorBoundaryServices } from '../../types';
|
||||
import { KibanaErrorBoundaryProviderDeps, KibanaErrorBoundaryServices } from '../../types';
|
||||
import { KibanaErrorService } from './error_service';
|
||||
|
||||
const Context = React.createContext<KibanaErrorBoundaryServices | null>(null);
|
||||
|
||||
/**
|
||||
* A Context Provider for Jest and Storybooks
|
||||
* @internal
|
||||
*/
|
||||
export const KibanaErrorBoundaryDepsProvider: FC<KibanaErrorBoundaryServices> = ({
|
||||
children,
|
||||
onClickRefresh,
|
||||
errorService,
|
||||
}) => {
|
||||
return <Context.Provider value={{ onClickRefresh, errorService }}>{children}</Context.Provider>;
|
||||
};
|
||||
}) => <Context.Provider value={{ onClickRefresh, errorService }}>{children}</Context.Provider>;
|
||||
|
||||
/**
|
||||
* Kibana-specific Provider that maps dependencies to services.
|
||||
* Provider that uses dependencies to give context to the KibanaErrorBoundary component
|
||||
* This provider is aware if services were already created from a higher level of the component tree
|
||||
* @public
|
||||
*/
|
||||
export const KibanaErrorBoundaryProvider: FC = ({ children }) => {
|
||||
const value: KibanaErrorBoundaryServices = useMemo(
|
||||
() => ({
|
||||
export const KibanaErrorBoundaryProvider: FC<KibanaErrorBoundaryProviderDeps> = ({
|
||||
children,
|
||||
analytics,
|
||||
}) => {
|
||||
const parentContext = useContext(Context);
|
||||
const value: KibanaErrorBoundaryServices = useMemo(() => {
|
||||
// FIXME: analytics dep is optional - know when not to overwrite
|
||||
if (parentContext) {
|
||||
return parentContext;
|
||||
}
|
||||
|
||||
return {
|
||||
onClickRefresh: () => window.location.reload(),
|
||||
errorService: new KibanaErrorService(),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
errorService: new KibanaErrorService({ analytics }),
|
||||
};
|
||||
}, [parentContext, analytics]);
|
||||
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* React hook for accessing pre-wired services.
|
||||
* Utility that provides context
|
||||
* @internal
|
||||
*/
|
||||
export function useErrorBoundary(): KibanaErrorBoundaryServices {
|
||||
const context = useContext(Context);
|
||||
|
|
|
@ -8,8 +8,11 @@
|
|||
|
||||
import { KibanaErrorService } from './error_service';
|
||||
|
||||
describe('KibanaErrorBoundary KibanaErrorService', () => {
|
||||
const service = new KibanaErrorService();
|
||||
describe('KibanaErrorBoundary Error Service', () => {
|
||||
const mockDeps = {
|
||||
analytics: { reportEvent: jest.fn() },
|
||||
};
|
||||
const service = new KibanaErrorService(mockDeps);
|
||||
|
||||
it('construction', () => {
|
||||
expect(service).toHaveProperty('registerError');
|
||||
|
@ -71,4 +74,23 @@ describe('KibanaErrorBoundary KibanaErrorService', () => {
|
|||
// should not be "ThrowIfError"
|
||||
expect(serviceError.name).toBe('BadComponent');
|
||||
});
|
||||
|
||||
it('captures the error event 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).toHaveBeenCalledWith('fatal-error-react', {
|
||||
component_name: 'OutrageousMaker',
|
||||
error_message: 'Error: This is an outrageous and fatal error',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,9 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ThrowIfError } from '../..';
|
||||
import { REACT_FATAL_ERROR_EVENT_TYPE } from '../../lib/telemetry_events';
|
||||
import { KibanaErrorBoundaryProviderDeps } from '../../types';
|
||||
import { ThrowIfError } from '../ui/throw_if_error';
|
||||
|
||||
const MATCH_CHUNK_LOADERROR = /ChunkLoadError/;
|
||||
|
||||
|
@ -18,12 +20,22 @@ interface ErrorServiceError {
|
|||
isFatal: boolean;
|
||||
}
|
||||
|
||||
interface Deps {
|
||||
analytics?: KibanaErrorBoundaryProviderDeps['analytics'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Kibana Error Boundary Services: Error Service
|
||||
* Each Error Boundary tracks an instance of this class
|
||||
* @internal
|
||||
*/
|
||||
export class KibanaErrorService {
|
||||
private analytics?: Deps['analytics'];
|
||||
|
||||
constructor(deps: Deps) {
|
||||
this.analytics = deps.analytics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the error fallback UI should appear as an apologetic but promising "Refresh" button,
|
||||
* or treated with "danger" coloring and include a detailed error message.
|
||||
|
@ -62,7 +74,6 @@ export class KibanaErrorService {
|
|||
|
||||
/**
|
||||
* Creates a decorated error object
|
||||
* TODO: capture telemetry
|
||||
*/
|
||||
public registerError(
|
||||
error: Error,
|
||||
|
@ -71,6 +82,18 @@ export class KibanaErrorService {
|
|||
const isFatal = this.getIsFatal(error);
|
||||
const name = this.getErrorComponentName(errorInfo);
|
||||
|
||||
try {
|
||||
if (isFatal) {
|
||||
this.analytics?.reportEvent(REACT_FATAL_ERROR_EVENT_TYPE, {
|
||||
component_name: name,
|
||||
error_message: error.toString(),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
errorInfo,
|
||||
|
|
|
@ -12,8 +12,9 @@ import React, { FC } from 'react';
|
|||
import { KibanaErrorBoundary } from '../..';
|
||||
import { BadComponent, ChunkLoadErrorComponent, getServicesMock } from '../../mocks';
|
||||
import { KibanaErrorBoundaryServices } from '../../types';
|
||||
import { errorMessageStrings as strings } from './message_strings';
|
||||
import { KibanaErrorBoundaryDepsProvider } from '../services/error_boundary_services';
|
||||
import { KibanaErrorService } from '../services/error_service';
|
||||
import { errorMessageStrings as strings } from './message_strings';
|
||||
|
||||
describe('<KibanaErrorBoundary>', () => {
|
||||
let services: KibanaErrorBoundaryServices;
|
||||
|
@ -72,4 +73,23 @@ describe('<KibanaErrorBoundary>', () => {
|
|||
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('captures the error event for 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).toHaveBeenCalledWith('fatal-error-react', {
|
||||
component_name: 'BadComponent',
|
||||
error_message: 'Error: This is an error to show the test user!',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
"kbn_references": [
|
||||
"@kbn/shared-ux-storybook-mock",
|
||||
"@kbn/i18n",
|
||||
"@kbn/core-analytics-browser-mocks",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -16,3 +16,15 @@ export interface KibanaErrorBoundaryServices {
|
|||
onClickRefresh: () => void;
|
||||
errorService: KibanaErrorService;
|
||||
}
|
||||
|
||||
/**
|
||||
* {analytics: AnalyticsServiceStart | undefined}
|
||||
* @public
|
||||
*/
|
||||
export interface KibanaErrorBoundaryProviderDeps {
|
||||
analytics:
|
||||
| {
|
||||
reportEvent: (eventType: string, eventData: object) => void;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { Observable } from 'rxjs';
|
|||
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import type { MountPoint } from '@kbn/core/public';
|
||||
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
|
||||
import type { I18nStart } from '@kbn/core-i18n-browser';
|
||||
import type { CoreTheme, ThemeServiceStart } from '@kbn/core-theme-browser';
|
||||
import { defaultTheme } from '@kbn/react-kibana-context-common';
|
||||
|
@ -35,6 +36,7 @@ const i18n: I18nStart = {
|
|||
* @deprecated use `ToMountPointParams` from `@kbn/react-kibana-mount`
|
||||
*/
|
||||
export interface ToMountPointOptions {
|
||||
analytics?: AnalyticsServiceStart;
|
||||
theme$?: Observable<CoreTheme>;
|
||||
}
|
||||
|
||||
|
@ -43,8 +45,8 @@ export interface ToMountPointOptions {
|
|||
*/
|
||||
export const toMountPoint = (
|
||||
node: React.ReactNode,
|
||||
{ theme$ }: ToMountPointOptions = {}
|
||||
{ analytics, theme$ }: ToMountPointOptions = {}
|
||||
): MountPoint => {
|
||||
const theme = theme$ ? { theme$ } : themeStart;
|
||||
return _toMountPoint(node, { theme, i18n });
|
||||
return _toMountPoint(node, { analytics, theme, i18n });
|
||||
};
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
"@kbn/react-kibana-context-common",
|
||||
"@kbn/react-kibana-context-styled",
|
||||
"@kbn/code-editor",
|
||||
"@kbn/core-analytics-browser",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -132,6 +132,7 @@ export const ManagementApp = ({
|
|||
setBreadcrumbs={setBreadcrumbsScoped}
|
||||
onAppMounted={onAppMounted}
|
||||
sections={sections}
|
||||
analytics={dependencies.coreStart.analytics}
|
||||
/>
|
||||
</KibanaPageTemplate>
|
||||
</KibanaThemeProvider>
|
||||
|
|
|
@ -9,7 +9,12 @@
|
|||
import React, { memo } from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { Router, Routes, Route } from '@kbn/shared-ux-router';
|
||||
import { AppMountParameters, ChromeBreadcrumb, ScopedHistory } from '@kbn/core/public';
|
||||
import {
|
||||
AnalyticsServiceStart,
|
||||
AppMountParameters,
|
||||
ChromeBreadcrumb,
|
||||
ScopedHistory,
|
||||
} from '@kbn/core/public';
|
||||
import { KibanaErrorBoundary, KibanaErrorBoundaryProvider } from '@kbn/shared-ux-error-boundary';
|
||||
import { ManagementAppWrapper } from '../management_app_wrapper';
|
||||
import { ManagementLandingPage } from '../landing';
|
||||
|
@ -21,12 +26,20 @@ interface ManagementRouterProps {
|
|||
setBreadcrumbs: (crumbs?: ChromeBreadcrumb[], appHistory?: ScopedHistory) => void;
|
||||
onAppMounted: (id: string) => void;
|
||||
sections: ManagementSection[];
|
||||
analytics: AnalyticsServiceStart;
|
||||
}
|
||||
|
||||
export const ManagementRouter = memo(
|
||||
({ history, setBreadcrumbs, onAppMounted, sections, theme$ }: ManagementRouterProps) => {
|
||||
({
|
||||
history,
|
||||
setBreadcrumbs,
|
||||
onAppMounted,
|
||||
sections,
|
||||
theme$,
|
||||
analytics,
|
||||
}: ManagementRouterProps) => {
|
||||
return (
|
||||
<KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundaryProvider analytics={analytics}>
|
||||
<KibanaErrorBoundary>
|
||||
<Router history={history}>
|
||||
<Routes>
|
||||
|
|
28
src/plugins/share/public/lib/registrations.ts
Normal file
28
src/plugins/share/public/lib/registrations.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-browser';
|
||||
import {
|
||||
REACT_FATAL_ERROR_EVENT_TYPE,
|
||||
ReactFatalError,
|
||||
reactFatalErrorSchema,
|
||||
} from '@kbn/shared-ux-error-boundary';
|
||||
|
||||
interface SetupDeps {
|
||||
analytics: AnalyticsServiceSetup;
|
||||
}
|
||||
|
||||
export const registrations = {
|
||||
setup(deps: SetupDeps) {
|
||||
// register event type for errors caught and reported by @kbn/shared-ux-error-boundary
|
||||
deps.analytics.registerEventType<ReactFatalError>({
|
||||
eventType: REACT_FATAL_ERROR_EVENT_TYPE,
|
||||
schema: reactFatalErrorSchema,
|
||||
});
|
||||
},
|
||||
};
|
|
@ -22,6 +22,7 @@ import type { BrowserShortUrlClient } from './url_service/short_urls/short_url_c
|
|||
import { AnonymousAccessServiceContract } from '../common';
|
||||
import { LegacyShortUrlLocatorDefinition } from '../common/url_service/locators/legacy_short_url_locator';
|
||||
import { ShortUrlRedirectLocatorDefinition } from '../common/url_service/locators/short_url_redirect_locator';
|
||||
import { registrations } from './lib/registrations';
|
||||
import type { BrowserUrlService } from './types';
|
||||
|
||||
/** @public */
|
||||
|
@ -68,7 +69,7 @@ export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
|
|||
constructor(private readonly initializerContext: PluginInitializerContext) {}
|
||||
|
||||
public setup(core: CoreSetup): SharePluginSetup {
|
||||
const { http } = core;
|
||||
const { analytics, http } = core;
|
||||
const { basePath } = http;
|
||||
|
||||
this.url = new UrlService<BrowserShortUrlClientFactoryCreateParams, BrowserShortUrlClient>({
|
||||
|
@ -106,6 +107,8 @@ export class SharePlugin implements Plugin<SharePluginSetup, SharePluginStart> {
|
|||
this.redirectManager.registerLocatorRedirectApp(core);
|
||||
this.redirectManager.registerLegacyShortUrlRedirectApp(core);
|
||||
|
||||
registrations.setup({ analytics });
|
||||
|
||||
return {
|
||||
...this.shareMenuRegistry.setup(),
|
||||
url: this.url,
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
"@kbn/core-custom-branding-browser",
|
||||
"@kbn/core-saved-objects-utils-server",
|
||||
"@kbn/react-kibana-context-theme",
|
||||
"@kbn/core-analytics-browser",
|
||||
"@kbn/shared-ux-error-boundary",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
|
|
|
@ -209,7 +209,7 @@ export function ObservabilityPageTemplate({
|
|||
: undefined
|
||||
}
|
||||
>
|
||||
<KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundaryProvider analytics={services.analytics}>
|
||||
<KibanaErrorBoundary>
|
||||
<KibanaPageTemplate.Section
|
||||
component="div"
|
||||
|
|
|
@ -51,6 +51,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
|
|||
const services = useKibana().services;
|
||||
const {
|
||||
i18n,
|
||||
analytics,
|
||||
application: { capabilities },
|
||||
uiActions,
|
||||
upselling,
|
||||
|
@ -59,7 +60,7 @@ const StartAppComponent: FC<StartAppComponent> = ({
|
|||
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);
|
||||
|
||||
return (
|
||||
<KibanaErrorBoundaryProvider>
|
||||
<KibanaErrorBoundaryProvider analytics={analytics}>
|
||||
<KibanaErrorBoundary>
|
||||
<i18n.Context>
|
||||
<ManageGlobalToaster>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue