UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

175 lines (154 loc) 5.2 kB
/** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: Apache-2.0 */ /** * @fileoverview Gathers all entries logged to the console, including console API calls, * exceptions, and browser reports (on violations, interventions, deprecations, etc.). */ import BaseGatherer from '../base-gatherer.js'; /** * @param {LH.Crdp.Runtime.RemoteObject} obj * @return {string} */ function remoteObjectToString(obj) { if (typeof obj.value !== 'undefined' || obj.type === 'undefined') { return String(obj.value); } if (typeof obj.description === 'string' && obj.description !== obj.className) { return obj.description; } const type = obj.subtype || obj.type; const className = obj.className || 'Object'; // Simulate calling String() on the object. return `[${type} ${className}]`; } class ConsoleMessages extends BaseGatherer { /** @type {LH.Gatherer.GathererMeta} */ meta = { supportedModes: ['timespan', 'navigation'], }; constructor() { super(); /** @type {LH.Artifacts.ConsoleMessage[]} */ this._logEntries = []; this._onConsoleAPICalled = this.onConsoleAPICalled.bind(this); this._onExceptionThrown = this.onExceptionThrown.bind(this); this._onLogEntryAdded = this.onLogEntry.bind(this); } /** * Handles events for when a script invokes a console API. * @param {LH.Crdp.Runtime.ConsoleAPICalledEvent} event */ onConsoleAPICalled(event) { const {type} = event; if (type !== 'warning' && type !== 'error') { // Only gather warnings and errors for brevity. return; } /** @type {LH.Crdp.Runtime.RemoteObject[]} */ const args = event.args || []; const text = args.map(remoteObjectToString).join(' '); if (!text && !event.stackTrace) { // No useful information from Chrome. Skip. return; } const {url, lineNumber, columnNumber} = event.stackTrace?.callFrames[0] || {}; /** @type {LH.Artifacts.ConsoleMessage} */ const consoleMessage = { eventType: 'consoleAPI', source: type === 'warning' ? 'console.warn' : 'console.error', level: type, text, stackTrace: event.stackTrace, timestamp: event.timestamp, url, lineNumber, columnNumber, }; this._logEntries.push(consoleMessage); } /** * Handles exception thrown events. * @param {LH.Crdp.Runtime.ExceptionThrownEvent} event */ onExceptionThrown(event) { const text = event.exceptionDetails.exception ? event.exceptionDetails.exception.description : event.exceptionDetails.text; if (!text) { return; } /** @type {LH.Artifacts.ConsoleMessage} */ const consoleMessage = { eventType: 'exception', source: 'exception', level: 'error', text, stackTrace: event.exceptionDetails.stackTrace, timestamp: event.timestamp, url: event.exceptionDetails.url, scriptId: event.exceptionDetails.scriptId, lineNumber: event.exceptionDetails.lineNumber, columnNumber: event.exceptionDetails.columnNumber, }; this._logEntries.push(consoleMessage); } /** * Handles browser reports logged to the console, including interventions, * deprecations, violations, and more. * @param {LH.Crdp.Log.EntryAddedEvent} event */ onLogEntry(event) { const {source, level, text, stackTrace, timestamp, url, lineNumber} = event.entry; // JS events have a stack trace, which we use to get the column. // CSS/HTML events only expose a line number. const firstStackFrame = event.entry.stackTrace?.callFrames[0]; this._logEntries.push({ eventType: 'protocolLog', source, level, text, stackTrace, timestamp, url, scriptId: firstStackFrame?.scriptId, lineNumber, columnNumber: firstStackFrame?.columnNumber, }); } /** * @param {LH.Gatherer.Context} passContext */ async startInstrumentation(passContext) { const session = passContext.driver.defaultSession; session.on('Log.entryAdded', this._onLogEntryAdded); await session.sendCommand('Log.enable'); await session.sendCommand('Log.startViolationsReport', { config: [{name: 'discouragedAPIUse', threshold: -1}], }); session.on('Runtime.consoleAPICalled', this._onConsoleAPICalled); session.on('Runtime.exceptionThrown', this._onExceptionThrown); await session.sendCommand('Runtime.enable'); } /** * @param {LH.Gatherer.Context} passContext * @return {Promise<void>} */ async stopInstrumentation({driver}) { await driver.defaultSession.sendCommand('Log.stopViolationsReport'); await driver.defaultSession.off('Log.entryAdded', this._onLogEntryAdded); await driver.defaultSession.sendCommand('Log.disable'); await driver.defaultSession.off('Runtime.consoleAPICalled', this._onConsoleAPICalled); await driver.defaultSession.off('Runtime.exceptionThrown', this._onExceptionThrown); await driver.defaultSession.sendCommand('Runtime.disable'); } /** * @return {Promise<LH.Artifacts['ConsoleMessages']>} */ async getArtifact() { return this._logEntries; } } export default ConsoleMessages;