Skip to content

Commit 39b3602

Browse files
committed
Track toast errors using apm-rum (elastic#217948)
Closes elastic/observability-dev#4022 ## Summary In this PR, we are capturing toast errors using apm-rum: https://github.com/user-attachments/assets/b61529f9-ab8e-4171-9042-0884e11eb385 ErrorType is available in labels which this feature was added to the rum agent in this [PR](elastic/apm-agent-rum-js#1594). ### 🧪 How to test Add the following to your kibana.yml file: ``` elastic.apm.active: true elastic.apm.transactionSampleRate: 1.0 elastic.apm.environment: yourName <-- Change to your name ``` <details> <summary>Throw a toast error</summary> Add this code to a page as [alerts page](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/observability/public/pages/alerts/alerts.tsx) and visit http://localhost:5601/kibana/app/observability/alerts ``` useEffect(() => { const error = new Error('Mary test error > toasts.addError'); toasts.addError(error, { title: 'Testing error toast', toastMessage: error.message }); toasts.addDanger('Testing danger toast'); }, []); ``` </details> Then visit [kibana-cloud-apm.elastic.dev](https://kibana-cloud-apm.elastic.dev/app/apm/services/kibana-frontend/errors?comparisonEnabled=true&environment=ENVIRONMENT_ALL&kuery=&latencyAggregationType=avg&offset=1d&rangeFrom=now-1h&rangeTo=now&serviceGroup=&transactionType=page-load) filtered for `yourName` in the environment. (cherry picked from commit ae9e5d6) # Conflicts: # src/core/packages/notifications/browser-internal/src/toasts/toasts_api.tsx
1 parent 3429d03 commit 39b3602

File tree

2 files changed

+52
-2
lines changed

2 files changed

+52
-2
lines changed

src/core/packages/notifications/browser-internal/src/toasts/toasts_api.test.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,19 @@ import { firstValueFrom } from 'rxjs';
1111

1212
import { ToastsApi } from './toasts_api';
1313

14+
import { apm } from '@elastic/apm-rum';
1415
import { uiSettingsServiceMock } from '@kbn/core-ui-settings-browser-mocks';
1516
import { analyticsServiceMock } from '@kbn/core-analytics-browser-mocks';
1617
import { i18nServiceMock } from '@kbn/core-i18n-browser-mocks';
1718
import { themeServiceMock } from '@kbn/core-theme-browser-mocks';
1819
import { userProfileServiceMock } from '@kbn/core-user-profile-browser-mocks';
1920

21+
jest.mock('@elastic/apm-rum', () => ({
22+
apm: {
23+
captureError: jest.fn(),
24+
},
25+
}));
26+
2027
async function getCurrentToasts(toasts: ToastsApi) {
2128
return await firstValueFrom(toasts.get$());
2229
}
@@ -213,36 +220,53 @@ describe('#addDanger()', () => {
213220
it('adds a danger toast', async () => {
214221
const toasts = new ToastsApi(toastDeps());
215222
expect(toasts.addDanger({})).toHaveProperty('color', 'danger');
223+
expect(apm.captureError).toBeCalledWith('No title or text is provided.', {
224+
labels: { errorType: 'ToastDanger' },
225+
});
216226
});
217227

218228
it('returns the created toast', async () => {
219229
const toasts = new ToastsApi(toastDeps());
220230
const toast = toasts.addDanger({});
221231
const currentToasts = await getCurrentToasts(toasts);
222232
expect(currentToasts[0]).toBe(toast);
233+
expect(apm.captureError).toBeCalledWith('No title or text is provided.', {
234+
labels: { errorType: 'ToastDanger' },
235+
});
223236
});
224237

225238
it('fallbacks to default values for undefined properties', async () => {
226239
const toasts = new ToastsApi(toastDeps());
227240
const toast = toasts.addDanger({ title: 'foo', toastLifeTimeMs: undefined });
228241
expect(toast.toastLifeTimeMs).toEqual(10000);
242+
expect(apm.captureError).toBeCalledWith('foo', {
243+
labels: { errorType: 'ToastDanger' },
244+
});
229245
});
230246
});
231247

232248
describe('#addError', () => {
233249
it('adds an error toast', async () => {
234250
const toasts = new ToastsApi(toastDeps());
235251
toasts.start(startDeps());
236-
const toast = toasts.addError(new Error('unexpected error'), { title: 'Something went wrong' });
252+
const error = new Error('unexpected error');
253+
const toast = toasts.addError(error, { title: 'Something went wrong' });
237254
expect(toast).toHaveProperty('color', 'danger');
238255
expect(toast).toHaveProperty('title', 'Something went wrong');
256+
expect(apm.captureError).toBeCalledWith(error, {
257+
labels: { errorType: 'ToastError' },
258+
});
239259
});
240260

241261
it('returns the created toast', async () => {
242262
const toasts = new ToastsApi(toastDeps());
243263
toasts.start(startDeps());
244-
const toast = toasts.addError(new Error('unexpected error'), { title: 'Something went wrong' });
264+
const error = new Error('unexpected error');
265+
const toast = toasts.addError(error, { title: 'Something went wrong' });
245266
const currentToasts = await getCurrentToasts(toasts);
246267
expect(currentToasts[0]).toBe(toast);
268+
expect(apm.captureError).toBeCalledWith(error, {
269+
labels: { errorType: 'ToastError' },
270+
});
247271
});
248272
});

src/core/packages/notifications/browser-internal/src/toasts/toasts_api.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import React from 'react';
1111
import * as Rx from 'rxjs';
1212
import { omitBy, isUndefined } from 'lodash';
1313

14+
import { apm } from '@elastic/apm-rum';
1415
import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser';
1516
import type { I18nStart } from '@kbn/core-i18n-browser';
1617
import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
@@ -37,6 +38,24 @@ const normalizeToast = (toastOrTitle: ToastInput): ToastInputFields => {
3738
return omitBy(toastOrTitle, isUndefined);
3839
};
3940

41+
const getToastTitleOrText = (toastOrTitle: ToastInput): string => {
42+
if (typeof toastOrTitle === 'string') {
43+
return toastOrTitle;
44+
} else if (typeof toastOrTitle.title === 'string') {
45+
return toastOrTitle.title;
46+
} else if (typeof toastOrTitle.text === 'string') {
47+
return toastOrTitle.text;
48+
}
49+
50+
return 'No title or text is provided.';
51+
};
52+
53+
const getApmLabels = (errorType: 'ToastError' | 'ToastDanger') => {
54+
return {
55+
errorType,
56+
};
57+
};
58+
4059
interface StartDeps {
4160
analytics: AnalyticsServiceStart;
4261
overlays: OverlayStart;
@@ -158,6 +177,10 @@ export class ToastsApi implements IToasts {
158177
* @returns a {@link Toast}
159178
*/
160179
public addDanger(toastOrTitle: ToastInput, options?: ToastOptions) {
180+
const toastTitle = getToastTitleOrText(toastOrTitle);
181+
apm.captureError(toastTitle, {
182+
labels: getApmLabels('ToastDanger'),
183+
});
161184
return this.add({
162185
color: 'danger',
163186
iconType: 'error',
@@ -175,6 +198,9 @@ export class ToastsApi implements IToasts {
175198
* @returns a {@link Toast}
176199
*/
177200
public addError(error: Error, options: ErrorToastOptions) {
201+
apm.captureError(error, {
202+
labels: getApmLabels('ToastError'),
203+
});
178204
const message = options.toastMessage || error.message;
179205
return this.add({
180206
color: 'danger',

0 commit comments

Comments
 (0)