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