/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", getFramesFromStack: "chrome://remote/content/shared/Stack.sys.mjs", Log: "chrome://remote/content/shared/Log.sys.mjs", }); XPCOMUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get()); /** * The ConsoleListener can be used to listen for console messages related to * Javascript errors, certain warnings which all happen within a specific * windowGlobal. Consumers can listen for the message types "error", * "warn" and "info". * * Example: * ``` * const onJavascriptError = (eventName, data = {}) => { * const { level, message, stacktrace, timestamp } = data; * ... * }; * * const listener = new ConsoleListener(innerWindowId); * listener.on("error", onJavascriptError); * listener.startListening(); * ... * listener.stopListening(); * ``` * * @fires message * The ConsoleListener emits "error", "warn" and "info" events, with the * following object as payload: * - {String} level - Importance, one of `info`, `warn`, `error`, * `debug`, `trace`. * - {String} message - Actual message from the console entry. * - {Array} stacktrace - List of stack frames, * starting from most recent. * - {Number} timeStamp - Timestamp when the method was called. */ export class ConsoleListener { #emittedMessages; #innerWindowId; #listening; /** * Create a new ConsolerListener instance. * * @param {number} innerWindowId * The inner window id to filter the messages for. */ constructor(innerWindowId) { lazy.EventEmitter.decorate(this); this.#emittedMessages = new Set(); this.#innerWindowId = innerWindowId; this.#listening = false; } get listening() { return this.#listening; } destroy() { this.stopListening(); this.#emittedMessages = null; } startListening() { if (this.#listening) { return; } Services.console.registerListener(this.#onConsoleMessage); // Emit cached messages after registering the listener, to make sure we // don't miss any message. this.#emitCachedMessages(); this.#listening = true; } stopListening() { if (!this.#listening) { return; } Services.console.unregisterListener(this.#onConsoleMessage); this.#listening = false; } #emitCachedMessages() { const cachedMessages = Services.console.getMessageArray() || []; for (const message of cachedMessages) { this.#onConsoleMessage(message); } } #onConsoleMessage = message => { if (!(message instanceof Ci.nsIScriptError)) { // For now ignore basic nsIConsoleMessage instances, which are only // relevant to Chrome code and do not have a valid window reference. return; } // Bail if this message was already emitted, useful to filter out cached // messages already received by the consumer. if (this.#emittedMessages.has(message)) { return; } this.#emittedMessages.add(message); if (message.innerWindowID !== this.#innerWindowId) { // If the message doesn't match the innerWindowId of the current context // ignore it. return; } const { errorFlag, warningFlag, infoFlag } = Ci.nsIScriptError; let level; if ((message.flags & warningFlag) == warningFlag) { level = "warn"; } else if ((message.flags & infoFlag) == infoFlag) { level = "info"; } else if ((message.flags & errorFlag) == errorFlag) { level = "error"; } else { lazy.logger.warn( `Not able to process console message with unknown flags ${message.flags}` ); return; } // Send event when actively listening. this.emit(level, { level, message: message.errorMessage, stacktrace: lazy.getFramesFromStack(message.stack), timeStamp: message.timeStamp, }); }; get QueryInterface() { return ChromeUtils.generateQI(["nsIConsoleListener"]); } }