diff --git a/__tests__/plugins/browser-console.test.ts b/__tests__/plugins/browser-console.test.ts
index 9a1884e1..04edb8a8 100644
--- a/__tests__/plugins/browser-console.test.ts
+++ b/__tests__/plugins/browser-console.test.ts
@@ -30,6 +30,7 @@ import { wsEndpoint } from '../utils/test-config';
describe('BrowserConsole', () => {
let server: Server;
+ const currentStep = { name: 'test-step', index: 0 };
beforeAll(async () => {
server = await Server.create();
});
@@ -43,7 +44,7 @@ describe('BrowserConsole', () => {
const { page } = driver;
browserConsole.start();
await page.goto(server.TEST_PAGE);
- browserConsole._currentStep = { name: 'step-name', index: 0 };
+ browserConsole._currentStep = currentStep;
await page.evaluate(() =>
console.warn('test-message', 1, { test: 'test' })
);
@@ -53,6 +54,58 @@ describe('BrowserConsole', () => {
expect(testMessage.text).toEqual(`test-message 1 {test: test}`);
expect(testMessage.type).toEqual('warning');
expect(testMessage.timestamp).toBeDefined();
- expect(testMessage.step).toEqual({ name: 'step-name', index: 0 });
+ expect(testMessage.step).toEqual(currentStep);
+ });
+
+ it('should capture browser page errors', async () => {
+ const driver = await Gatherer.setupDriver({ wsEndpoint });
+ const browserConsole = new BrowserConsole(driver);
+ const { page } = driver;
+ browserConsole.start();
+ await page.goto(server.TEST_PAGE);
+ browserConsole._currentStep = currentStep;
+ await page.setContent(`
+
+ `);
+ await page.waitForLoadState('networkidle');
+ const messages = browserConsole.stop();
+ await Gatherer.stop();
+
+ const notFoundMessage = messages.find(
+ m => m.text.indexOf('Failed to load resource:') >= 0
+ );
+ expect(notFoundMessage.text).toEqual(
+ `Failed to load resource: the server responded with a status of 404 (Not Found)`
+ );
+ expect(notFoundMessage.type).toEqual('error');
+ expect(notFoundMessage.step).toEqual(currentStep);
+
+ const referenceError = messages.find(
+ m => m.text.indexOf('that is not defined') >= 0
+ );
+ expect(referenceError.error.stack).toContain(
+ `ReferenceError: that is not defined\n at HTMLImageElement.onerror`
+ );
+ expect(referenceError.type).toEqual('error');
+ expect(referenceError.step).toEqual(currentStep);
+ });
+
+ it('should capture unhandled rejections', async () => {
+ const driver = await Gatherer.setupDriver({ wsEndpoint });
+ const browserConsole = new BrowserConsole(driver);
+ browserConsole.start();
+ browserConsole._currentStep = currentStep;
+ await driver.page.goto(server.TEST_PAGE);
+ await driver.page.setContent(
+ ``
+ );
+ await driver.page.waitForLoadState('networkidle');
+ const messages = browserConsole.stop();
+ await Gatherer.stop();
+
+ const unhandledError = messages.find(m => m.text.indexOf('Boom') >= 0);
+ expect(unhandledError.type).toEqual('error');
+ expect(unhandledError.error.stack).toContain('Error: Boom');
+ expect(unhandledError.step).toEqual(currentStep);
});
});
diff --git a/__tests__/reporters/__snapshots__/json.test.ts.snap b/__tests__/reporters/__snapshots__/json.test.ts.snap
index ed4f2c41..50866640 100644
--- a/__tests__/reporters/__snapshots__/json.test.ts.snap
+++ b/__tests__/reporters/__snapshots__/json.test.ts.snap
@@ -356,6 +356,7 @@ exports[`json reporter writes each step as NDJSON to the FD 1`] = `
{\\"type\\":\\"step/filmstrips\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"s1\\",\\"index\\":1},\\"root_fields\\":{\\"browser\\":{\\"relative_trace\\":{\\"start\\":{\\"us\\":392583998697}}},\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"index\\":0},\\"blob\\":\\"dummy\\",\\"blob_mime\\":\\"image/jpeg\\",\\"package_version\\":\\"0.0.1\\"}
{\\"type\\":\\"step/end\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"s1\\",\\"index\\":1,\\"status\\":\\"succeeded\\",\\"duration\\":{\\"us\\":10000000}},\\"root_fields\\":{\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"source\\":\\"() => { }\\",\\"url\\":\\"dummy\\",\\"status\\":\\"succeeded\\"},\\"url\\":\\"dummy\\",\\"package_version\\":\\"0.0.1\\"}
{\\"type\\":\\"journey/network_info\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"root_fields\\":{\\"user_agent\\":{},\\"http\\":{\\"request\\":{\\"body\\":{\\"bytes\\":0,\\"content\\":\\"\\"}}},\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"browser\\":{},\\"is_navigation_request\\":true},\\"package_version\\":\\"0.0.1\\"}
+{\\"type\\":\\"journey/browserconsole\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\"},\\"step\\":{\\"name\\":\\"step-name\\",\\"index\\":0},\\"root_fields\\":{\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"text\\":\\"Boom\\",\\"type\\":\\"error\\"},\\"error\\":{\\"name\\":\\"Error\\",\\"message\\":\\"boom\\",\\"stack\\":\\"\\"},\\"package_version\\":\\"0.0.1\\"}
{\\"type\\":\\"journey/end\\",\\"@timestamp\\":1600300800000000,\\"journey\\":{\\"name\\":\\"j1\\",\\"id\\":\\"j1\\",\\"status\\":\\"succeeded\\"},\\"root_fields\\":{\\"os\\":{\\"platform\\":\\"darwin\\"},\\"package\\":{\\"name\\":\\"@elastic/synthetics\\",\\"version\\":\\"0.0.1\\"}},\\"payload\\":{\\"start\\":0,\\"end\\":11,\\"status\\":\\"succeeded\\"},\\"package_version\\":\\"0.0.1\\"}
"
`;
diff --git a/__tests__/reporters/json.test.ts b/__tests__/reporters/json.test.ts
index ba492263..44b62614 100644
--- a/__tests__/reporters/json.test.ts
+++ b/__tests__/reporters/json.test.ts
@@ -103,6 +103,8 @@ describe('json reporter', () => {
};
it('writes each step as NDJSON to the FD', async () => {
+ const error = new Error('boom');
+ error.stack = '';
runner.emit('journey:register', {
journey: j1,
});
@@ -173,6 +175,15 @@ describe('json reporter', () => {
browser: {},
} as any,
],
+ browserconsole: [
+ {
+ timestamp,
+ text: 'Boom',
+ type: 'error',
+ step: { name: 'step-name', index: 0 },
+ error,
+ },
+ ],
});
runner.emit('end', 'done');
expect((await readAndCloseStream()).toString()).toMatchSnapshot();
diff --git a/src/common_types.ts b/src/common_types.ts
index 2f70bd5e..e6e446bb 100644
--- a/src/common_types.ts
+++ b/src/common_types.ts
@@ -119,6 +119,7 @@ export type NetworkInfo = {
export type BrowserMessage = {
text: string;
type: string;
+ error?: Error;
} & DefaultPluginOutput;
export type PluginOutput = {
diff --git a/src/plugins/browser-console.ts b/src/plugins/browser-console.ts
index f5ce280e..82572ce4 100644
--- a/src/plugins/browser-console.ts
+++ b/src/plugins/browser-console.ts
@@ -48,18 +48,41 @@ export class BrowserConsole {
type,
step: { name, index },
});
- if (this.messages.length > defaultMessageLimit) {
- this.messages.splice(0, 1);
- }
+
+ this.enforceMessagesLimit();
+ }
+ };
+
+ private pageErrorEventListener = (error: Error) => {
+ if (!this._currentStep) {
+ return;
}
+ const { name, index } = this._currentStep;
+ this.messages.push({
+ timestamp: getTimestamp(),
+ text: error.message,
+ type: 'error',
+ step: { name, index },
+ error,
+ });
+
+ this.enforceMessagesLimit();
};
+ private enforceMessagesLimit() {
+ if (this.messages.length > defaultMessageLimit) {
+ this.messages.splice(0, 1);
+ }
+ }
+
start() {
this.driver.page.on('console', this.consoleEventListener);
+ this.driver.page.on('pageerror', this.pageErrorEventListener);
}
stop() {
this.driver.page.off('console', this.consoleEventListener);
+ this.driver.page.off('pageerror', this.pageErrorEventListener);
return this.messages;
}
}
diff --git a/src/reporters/json.ts b/src/reporters/json.ts
index 08881a53..944c656f 100644
--- a/src/reporters/json.ts
+++ b/src/reporters/json.ts
@@ -483,13 +483,17 @@ export default class JSONReporter extends BaseReporter {
});
}
if (browserconsole) {
- browserconsole.forEach(({ timestamp, text, type, step }) => {
+ browserconsole.forEach(({ timestamp, text, type, step, error }) => {
this.writeJSON({
type: 'journey/browserconsole',
journey,
timestamp,
step,
- payload: { text, type } as Payload,
+ error,
+ payload: {
+ text,
+ type,
+ } as Payload,
});
});
}
@@ -554,7 +558,7 @@ export default class JSONReporter extends BaseReporter {
});
}
- // Writes a structered synthetics event
+ // Writes a structured synthetics event
// Note that blob is ultimately stored in ES as a base64 encoded string. You must base 64 encode
// it before passing it into this function!
// The payload field is an un-indexed field with no ES mapping, so users can put arbitary structured