Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 58 additions & 1 deletion special-pages/pages/history/integration-tests/history.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test } from '@playwright/test';
import { test, expect } from '@playwright/test';
import { HistoryTestPage } from './history.page.js';

test.describe('history', () => {
Expand Down Expand Up @@ -263,4 +263,61 @@ test.describe('history', () => {
await hp.hasBackgroundColor({ hex: '#27282A' });
});
});

test.describe('global error listeners', () => {
test('reports uncaught errors via reportInitException', async ({ page }, workerInfo) => {
const hp = HistoryTestPage.create(page, workerInfo).withEntries(0);
await hp.openPage();
await hp.hasEmptyState();

await page.evaluate(() => {
setTimeout(() => {
throw new Error('test uncaught error');
}, 0);
});

const calls = await hp.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
context: 'specialPages',
featureName: 'history',
method: 'reportInitException',
params: { message: '[uncaught] test uncaught error' },
},
},
]);
});

test('reports unhandled rejections via reportInitException', async ({ page }, workerInfo) => {
const hp = HistoryTestPage.create(page, workerInfo).withEntries(0);
await hp.openPage();
await hp.hasEmptyState();

await page.evaluate(() => {
Promise.reject(new Error('test unhandled rejection'));
});

const calls = await hp.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
context: 'specialPages',
featureName: 'history',
method: 'reportInitException',
params: { message: '[unhandledrejection] test unhandled rejection' },
},
},
]);
});

test('does not fire reportInitException during normal page load', async ({ page }, workerInfo) => {
const hp = HistoryTestPage.create(page, workerInfo).withEntries(0);
await hp.openPage();
await hp.hasEmptyState();

const calls = await hp.mocks.outgoing({ names: ['reportInitException'] });
expect(calls).toHaveLength(0);
});
});
});
10 changes: 10 additions & 0 deletions special-pages/pages/history/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,16 @@ const messaging = createSpecialPageMessaging({

const historyPage = new HistoryPage(messaging);

window.addEventListener('error', (event) => {
const message = event.error?.message || event.message || 'unknown error';
historyPage.reportInitException({ message: `[uncaught] ${message}` });
});

window.addEventListener('unhandledrejection', (event) => {
const message = event.reason?.message || String(event.reason) || 'unknown rejection';
historyPage.reportInitException({ message: `[unhandledrejection] ${message}` });
});

/**
* Grab the root element from the index.html file - bail early if it's absent
*/
Expand Down
60 changes: 60 additions & 0 deletions special-pages/pages/new-tab/integration-tests/new-tab.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,64 @@ test.describe('newtab widgets', () => {
await ntp.hasBackgroundColor({ hex: '#000000' });
});
});

test.describe('global error listeners', () => {
test('reports uncaught errors via reportInitException', async ({ page }, workerInfo) => {
const ntp = NewtabPage.create(page, workerInfo);
await ntp.reducedMotion();
await ntp.openPage();
await ntp.waitForCustomizer();

await page.evaluate(() => {
setTimeout(() => {
throw new Error('test uncaught error');
}, 0);
});

const calls = await ntp.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
context: 'specialPages',
featureName: 'newTabPage',
method: 'reportInitException',
params: { message: '[uncaught] test uncaught error' },
},
},
]);
});

test('reports unhandled rejections via reportInitException', async ({ page }, workerInfo) => {
const ntp = NewtabPage.create(page, workerInfo);
await ntp.reducedMotion();
await ntp.openPage();
await ntp.waitForCustomizer();

await page.evaluate(() => {
Promise.reject(new Error('test unhandled rejection'));
});

const calls = await ntp.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
context: 'specialPages',
featureName: 'newTabPage',
method: 'reportInitException',
params: { message: '[unhandledrejection] test unhandled rejection' },
},
},
]);
});

test('does not fire reportInitException during normal page load', async ({ page }, workerInfo) => {
const ntp = NewtabPage.create(page, workerInfo);
await ntp.reducedMotion();
await ntp.openPage();
await ntp.waitForCustomizer();

const calls = await ntp.mocks.outgoing({ names: ['reportInitException'] });
expect(calls).toHaveLength(0);
});
});
});
10 changes: 10 additions & 0 deletions special-pages/pages/new-tab/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ const rawMessaging = createSpecialPageMessaging({
const { messaging, telemetry } = install(rawMessaging);
const newTabMessaging = new NewTabPage(messaging, import.meta.injectName);

window.addEventListener('error', (event) => {
const message = event.error?.message || event.message || 'unknown error';
newTabMessaging.reportInitException(`[uncaught] ${message}`);
});

window.addEventListener('unhandledrejection', (event) => {
const message = event.reason?.message || String(event.reason) || 'unknown rejection';
newTabMessaging.reportInitException(`[unhandledrejection] ${message}`);
});

/**
* Grab the root element from the index.html file - bail early if it's absent
*/
Expand Down
10 changes: 10 additions & 0 deletions special-pages/pages/onboarding/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ const messaging = createSpecialPageMessaging({

const onboarding = new OnboardingMessages(messaging, baseEnvironment.injectName);

window.addEventListener('error', (event) => {
const message = event.error?.message || event.message || 'unknown error';
onboarding.reportInitException({ message: `[uncaught] ${message}` });
});

window.addEventListener('unhandledrejection', (event) => {
const message = event.reason?.message || String(event.reason) || 'unknown rejection';
onboarding.reportInitException({ message: `[unhandledrejection] ${message}` });
});

async function init() {
const result = await callWithRetry(() => onboarding.init());
if ('error' in result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class OnboardingPage {
stepCompleted: {},
setAdBlocking: {},
reportPageException: {},
reportInitException: {},
init: {
stepDefinitions: {
systemSettings: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,4 +486,62 @@ test.describe('onboarding v4', () => {
});
});
});

test.describe('global error listeners', () => {
test('reports uncaught errors via reportInitException', async ({ page }, workerInfo) => {
const onboarding = OnboardingV4Page.create(page, workerInfo);
await onboarding.reducedMotion();
await onboarding.openPage({ env: 'app' });

await page.evaluate(() => {
setTimeout(() => {
throw new Error('test uncaught error');
}, 0);
});

const calls = await onboarding.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
context: 'specialPages',
featureName: 'onboarding',
method: 'reportInitException',
params: { message: '[uncaught] test uncaught error' },
},
},
]);
});

test('reports unhandled rejections via reportInitException', async ({ page }, workerInfo) => {
const onboarding = OnboardingV4Page.create(page, workerInfo);
await onboarding.reducedMotion();
await onboarding.openPage({ env: 'app' });

await page.evaluate(() => {
Promise.reject(new Error('test unhandled rejection'));
});

const calls = await onboarding.mocks.waitForCallCount({ method: 'reportInitException', count: 1 });
expect(calls).toMatchObject([
{
payload: {
context: 'specialPages',
featureName: 'onboarding',
method: 'reportInitException',
params: { message: '[unhandledrejection] test unhandled rejection' },
},
},
]);
});

test('does not fire reportInitException during normal page load', async ({ page }, workerInfo) => {
const onboarding = OnboardingV4Page.create(page, workerInfo);
await onboarding.reducedMotion();
await onboarding.openPage({ env: 'app' });

await page.waitForTimeout(200);
const calls = await onboarding.mocks.outgoing({ names: ['reportInitException'] });
expect(calls).toHaveLength(0);
});
});
});
Loading