1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * Define a 'console' API to roughly match the implementation provided by
8 * This module helps cases where code is shared between the web and Firefox.
9 * See also Browser.sys.mjs for an implementation of other web constants to help
10 * sharing code between the web and firefox;
12 * The API is only be a rough approximation for 3 reasons:
13 * - The Firebug console API is implemented in many places with differences in
14 * the implementations, so there isn't a single reference to adhere to
15 * - The Firebug console is a rich display compared with dump(), so there will
16 * be many things that we can't replicate
17 * - The primary use of this API is debugging and error logging so the perfect
18 * implementation isn't always required (or even well defined)
21 var gTimerRegistry = new Map();
24 * String utility to ensure that strings are a specified length. Strings
25 * that are too long are truncated to the max length and the last char is
26 * set to "_". Strings that are too short are padded with spaces.
28 * @param {string} aStr
29 * The string to format to the correct length
30 * @param {number} aMaxLen
31 * The maximum allowed length of the returned string
32 * @param {number} aMinLen (optional)
33 * The minimum allowed length of the returned string. If undefined,
34 * then aMaxLen will be used
35 * @param {object} aOptions (optional)
36 * An object allowing format customization. Allowed customizations:
37 * 'truncate' - can take the value "start" to truncate strings from
38 * the start as opposed to the end or "center" to truncate
39 * strings in the center.
40 * 'align' - takes an alignment when padding is needed for MinLen,
41 * either "start" or "end". Defaults to "start".
43 * The original string formatted to fit the specified lengths
45 function fmt(aStr, aMaxLen, aMinLen, aOptions) {
46 if (aMinLen == null) {
52 if (aStr.length > aMaxLen) {
53 if (aOptions && aOptions.truncate == "start") {
54 return "_" + aStr.substring(aStr.length - aMaxLen + 1);
55 } else if (aOptions && aOptions.truncate == "center") {
56 let start = aStr.substring(0, aMaxLen / 2);
58 let end = aStr.substring(aStr.length - aMaxLen / 2 + 1);
59 return start + "_" + end;
61 return aStr.substring(0, aMaxLen - 1) + "_";
63 if (aStr.length < aMinLen) {
64 let padding = Array(aMinLen - aStr.length + 1).join(" ");
65 aStr = aOptions.align === "end" ? padding + aStr : aStr + padding;
71 * Utility to extract the constructor name of an object.
72 * Object.toString gives: "[object ?????]"; we want the "?????".
74 * @param {object} aObj
75 * The object from which to extract the constructor name
77 * The constructor name
79 function getCtorName(aObj) {
83 if (aObj === undefined) {
86 if (aObj.constructor && aObj.constructor.name) {
87 return aObj.constructor.name;
89 // If that fails, use Objects toString which sometimes gives something
90 // better than 'Object', and at least defaults to Object if nothing better
91 return Object.prototype.toString.call(aObj).slice(8, -1);
95 * Indicates whether an object is a JS or `Components.Exception` error.
97 * @param {object} aThing
100 Is this object an error?
102 function isError(aThing) {
105 ((typeof aThing.name == "string" && aThing.name.startsWith("NS_ERROR_")) ||
106 getCtorName(aThing).endsWith("Error"))
111 * A single line stringification of an object designed for use by humans
113 * @param {any} aThing
114 * The object to be stringified
115 * @param {boolean} aAllowNewLines
117 * A single line representation of aThing, which will generally be at
120 function stringify(aThing, aAllowNewLines) {
121 if (aThing === undefined) {
125 if (aThing === null) {
129 if (isError(aThing)) {
130 return "Message: " + aThing;
133 if (typeof aThing == "object") {
134 let type = getCtorName(aThing);
135 if (Element.isInstance(aThing)) {
136 return debugElement(aThing);
138 type = type == "Object" ? "" : type + " ";
141 json = JSON.stringify(aThing);
143 // Can't use a real ellipsis here, because cmd.exe isn't unicode-enabled
144 json = "{" + Object.keys(aThing).join(":..,") + ":.., }";
149 if (typeof aThing == "function") {
150 return aThing.toString().replace(/\s+/g, " ");
153 let str = aThing.toString();
154 if (!aAllowNewLines) {
155 str = str.replace(/\n/g, "|");
161 * Create a simple debug representation of a given element.
163 * @param {Element} aElement
164 * The element to debug
166 * A simple single line representation of aElement
168 function debugElement(aElement) {
172 (aElement.id ? "#" + aElement.id : "") +
173 (aElement.className && aElement.className.split
174 ? "." + aElement.className.split(" ").join(" .")
181 * A multi line stringification of an object, designed for use by humans
183 * @param {any} aThing
184 * The object to be stringified
186 * A multi line representation of aThing
188 function log(aThing) {
189 if (aThing === null) {
193 if (aThing === undefined) {
194 return "undefined\n";
197 if (typeof aThing == "object") {
199 let type = getCtorName(aThing);
202 for (let [key, value] of aThing) {
203 reply += logProperty(key, value);
205 } else if (type == "Set") {
208 for (let value of aThing) {
209 reply += logProperty("" + i, value);
212 } else if (isError(aThing)) {
213 reply += " Message: " + aThing + "\n";
215 reply += " Stack:\n";
216 var frame = aThing.stack;
218 reply += " " + frame + "\n";
219 frame = frame.caller;
222 } else if (Element.isInstance(aThing)) {
223 reply += " " + debugElement(aThing) + "\n";
225 let keys = Object.getOwnPropertyNames(aThing);
227 reply += type + "\n";
228 keys.forEach(function (aProp) {
229 reply += logProperty(aProp, aThing[aProp]);
232 reply += type + "\n";
235 while (root != null) {
236 let properties = Object.keys(root);
238 properties.forEach(function (property) {
239 if (!(property in logged)) {
240 logged[property] = property;
241 reply += logProperty(property, aThing[property]);
245 root = Object.getPrototypeOf(root);
247 reply += " - prototype " + getCtorName(root) + "\n";
256 return " " + aThing.toString() + "\n";
260 * Helper for log() which converts a property/value pair into an output
263 * @param {string} aProp
264 * The name of the property to include in the output string
265 * @param {object} aValue
266 * Value assigned to aProp to be converted to a single line string
268 * Multi line output string describing the property/value pair
270 function logProperty(aProp, aValue) {
272 if (aProp == "stack" && typeof value == "string") {
273 let trace = parseStack(aValue);
274 reply += formatTrace(trace);
276 reply += " - " + aProp + " = " + stringify(aValue) + "\n";
282 all: Number.MIN_VALUE,
299 off: Number.MAX_VALUE,
303 * Helper to tell if a console message of `aLevel` type
304 * should be logged in stdout and sent to consoles given
305 * the current maximum log level being defined in `console.maxLogLevel`
307 * @param {string} aLevel
308 * Console message log level
309 * @param {string} aMaxLevel {string}
310 * String identifier (See LOG_LEVELS for possible
311 * values) that allows to filter which messages
312 * are logged based on their log level
314 * Should this message be logged or not?
316 function shouldLog(aLevel, aMaxLevel) {
317 return LOG_LEVELS[aMaxLevel] <= LOG_LEVELS[aLevel];
321 * Parse a stack trace, returning an array of stack frame objects, where
322 * each has filename/lineNumber/functionName members
324 * @param {string} aStack
325 * The serialized stack trace
327 * Array of { file: "...", line: NNN, call: "..." } objects
329 function parseStack(aStack) {
331 aStack.split("\n").forEach(function (line) {
335 let at = line.lastIndexOf("@");
336 let posn = line.substring(at + 1);
338 filename: posn.split(":")[0],
339 lineNumber: posn.split(":")[1],
340 functionName: line.substring(0, at),
347 * Format a frame coming from Components.stack such that it can be used by the
348 * Browser Console, via ConsoleAPIStorage notifications.
350 * @param {object} aFrame
351 * The stack frame from which to begin the walk.
352 * @param {number=0} aMaxDepth
353 * Maximum stack trace depth. Default is 0 - no depth limit.
355 * An array of {filename, lineNumber, functionName, language} objects.
356 * These objects follow the same format as other ConsoleAPIStorage
359 function getStack(aFrame, aMaxDepth = 0) {
361 aFrame = Components.stack.caller;
366 filename: aFrame.filename,
367 lineNumber: aFrame.lineNumber,
368 functionName: aFrame.name,
369 language: aFrame.language,
371 if (aMaxDepth == trace.length) {
374 aFrame = aFrame.caller;
380 * Take the output from parseStack() and convert it to nice readable
383 * @param {object[]} aTrace
384 * Array of trace objects as created by parseStack()
385 * @return {string} Multi line report of the stack trace
387 function formatTrace(aTrace) {
389 aTrace.forEach(function (frame) {
391 fmt(frame.filename, 20, 20, { truncate: "start" }) +
393 fmt(frame.lineNumber, 5, 5) +
395 fmt(frame.functionName, 75, 0, { truncate: "center" }) +
402 * Create a new timer by recording the current time under the specified name.
404 * @param {string} aName
405 * The name of the timer.
406 * @param {number} [aTimestamp=Date.now()]
407 * Optional timestamp that tells when the timer was originally started.
409 * The name property holds the timer name and the started property
410 * holds the time the timer was started. In case of error, it returns
411 * an object with the single property "error" that contains the key
412 * for retrieving the localized error message.
414 function startTimer(aName, aTimestamp) {
415 let key = aName.toString();
416 if (!gTimerRegistry.has(key)) {
417 gTimerRegistry.set(key, aTimestamp || Date.now());
419 return { name: aName, started: gTimerRegistry.get(key) };
423 * Stop the timer with the specified name and retrieve the elapsed time.
425 * @param {string} aName
426 * The name of the timer.
427 * @param {number} [aTimestamp=Date.now()]
428 * Optional timestamp that tells when the timer was originally stopped.
430 * The name property holds the timer name and the duration property
431 * holds the number of milliseconds since the timer was started.
433 function stopTimer(aName, aTimestamp) {
434 let key = aName.toString();
435 let duration = (aTimestamp || Date.now()) - gTimerRegistry.get(key);
436 gTimerRegistry.delete(key);
437 return { name: aName, duration };
441 * Dump a new message header to stdout by taking care of adding an eventual
444 * @param {object} aConsole
445 * ConsoleAPI instance
446 * @param {string} aLevel
447 * The string identifier for the message log level
448 * @param {string} aMessage
449 * The string message to print to stdout
451 function dumpMessage(aConsole, aLevel, aMessage) {
456 (aConsole.prefix ? aConsole.prefix + ": " : "") +
463 * Create a function which will output a concise level of output when used
464 * as a logging function
466 * @param {string} aLevel
467 * A prefix to all output generated from this function detailing the
468 * level at which output occurred
471 * @see createMultiLineDumper()
473 function createDumper(aLevel) {
475 if (!shouldLog(aLevel, this.maxLogLevel)) {
478 let args = Array.prototype.slice.call(arguments, 0);
479 let frame = getStack(Components.stack.caller, 1)[0];
480 sendConsoleAPIMessage(this, aLevel, frame, args);
481 let data = args.map(function (arg) {
482 return stringify(arg, true);
484 dumpMessage(this, aLevel, data.join(" "));
489 * Create a function which will output more detailed level of output when
490 * used as a logging function
492 * @param {string} aLevel
493 * A prefix to all output generated from this function detailing the
494 * level at which output occurred
497 * @see createDumper()
499 function createMultiLineDumper(aLevel) {
501 if (!shouldLog(aLevel, this.maxLogLevel)) {
504 dumpMessage(this, aLevel, "");
505 let args = Array.prototype.slice.call(arguments, 0);
506 let frame = getStack(Components.stack.caller, 1)[0];
507 sendConsoleAPIMessage(this, aLevel, frame, args);
508 args.forEach(function (arg) {
515 * Send a Console API message. This function will send a notification through
516 * the nsIConsoleAPIStorage service.
518 * @param {object} aConsole
519 * The instance of ConsoleAPI performing the logging.
520 * @param {string} aLevel
521 * Message severity level. This is usually the name of the console method
523 * @param {object} aFrame
524 * The youngest stack frame coming from Components.stack, as formatted by
526 * @param {array} aArgs
527 * The arguments given to the console method.
528 * @param {object} aOptions
529 * Object properties depend on the console method that was invoked:
530 * - timer: for time() and timeEnd(). Holds the timer information.
531 * - groupName: for group(), groupCollapsed() and groupEnd().
532 * - stacktrace: for trace(). Holds the array of stack frames as given by
535 function sendConsoleAPIMessage(aConsole, aLevel, aFrame, aArgs, aOptions = {}) {
538 innerID: aConsole.innerID || aFrame.filename,
539 consoleID: aConsole.consoleID,
541 filename: aFrame.filename,
542 lineNumber: aFrame.lineNumber,
543 functionName: aFrame.functionName,
544 timeStamp: Date.now(),
546 prefix: aConsole.prefix,
550 consoleEvent.wrappedJSObject = consoleEvent;
554 consoleEvent.stacktrace = aOptions.stacktrace;
558 consoleEvent.timer = aOptions.timer;
561 case "groupCollapsed":
564 consoleEvent.groupName = Array.prototype.join.call(aArgs, " ");
567 console.error(ex.stack);
573 let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(
574 Ci.nsIConsoleAPIStorage
576 if (ConsoleAPIStorage) {
577 ConsoleAPIStorage.recordEvent("jsm", consoleEvent);
582 * This creates a console object that somewhat replicates Firebug's console
585 * @param {object} aConsoleOptions
586 * Optional dictionary with a set of runtime console options:
587 * - prefix {string} : An optional prefix string to be printed before
588 * the actual logged message
589 * - maxLogLevel {string} : String identifier (See LOG_LEVELS for
590 * possible values) that allows to filter which
591 * messages are logged based on their log level.
592 * If falsy value, all messages will be logged.
593 * If wrong value that doesn't match any key of
594 * LOG_LEVELS, no message will be logged
595 * - maxLogLevelPref {string} : String pref name which contains the
596 * level to use for maxLogLevel. If the pref doesn't
597 * exist or gets removed, the maxLogLevel will default
598 * to the value passed to this constructor (or "all"
599 * if it wasn't specified).
600 * - dump {function} : An optional function to intercept all strings
602 * - innerID {string}: An ID representing the source of the message.
603 * Normally the inner ID of a DOM window.
604 * - consoleID {string} : String identified for the console, this will
605 * be passed through the console notifications
607 * A console API instance object
609 export function ConsoleAPI(aConsoleOptions = {}) {
610 // Normalize console options to set default values
611 // in order to avoid runtime checks on each console method call.
612 this.dump = aConsoleOptions.dump || dump;
613 this.prefix = aConsoleOptions.prefix || "";
614 this.maxLogLevel = aConsoleOptions.maxLogLevel;
615 this.innerID = aConsoleOptions.innerID || null;
616 this.consoleID = aConsoleOptions.consoleID || "";
618 // Setup maxLogLevelPref watching
619 let updateMaxLogLevel = () => {
621 Services.prefs.getPrefType(aConsoleOptions.maxLogLevelPref) ==
622 Services.prefs.PREF_STRING
624 this._maxLogLevel = Services.prefs
625 .getCharPref(aConsoleOptions.maxLogLevelPref)
628 this._maxLogLevel = this._maxExplicitLogLevel;
632 if (aConsoleOptions.maxLogLevelPref) {
634 Services.prefs.addObserver(
635 aConsoleOptions.maxLogLevelPref,
640 // Bind all the functions to this object.
641 for (let prop in this) {
642 if (typeof this[prop] === "function") {
643 this[prop] = this[prop].bind(this);
648 ConsoleAPI.prototype = {
650 * The last log level that was specified via the constructor or setter. This
651 * is used as a fallback if the pref doesn't exist or is removed.
653 _maxExplicitLogLevel: null,
655 * The current log level via all methods of setting (pref or via the API).
658 debug: createMultiLineDumper("debug"),
659 assert: createDumper("assert"),
660 log: createDumper("log"),
661 info: createDumper("info"),
662 warn: createDumper("warn"),
663 error: createMultiLineDumper("error"),
664 exception: createMultiLineDumper("error"),
666 trace: function Console_trace() {
667 if (!shouldLog("trace", this.maxLogLevel)) {
670 let args = Array.prototype.slice.call(arguments, 0);
671 let trace = getStack(Components.stack.caller);
672 sendConsoleAPIMessage(this, "trace", trace[0], args, { stacktrace: trace });
673 dumpMessage(this, "trace", "\n" + formatTrace(trace));
675 clear: function Console_clear() {},
677 dir: createMultiLineDumper("dir"),
678 dirxml: createMultiLineDumper("dirxml"),
679 group: createDumper("group"),
680 groupEnd: createDumper("groupEnd"),
682 time: function Console_time() {
683 if (!shouldLog("time", this.maxLogLevel)) {
686 let args = Array.prototype.slice.call(arguments, 0);
687 let frame = getStack(Components.stack.caller, 1)[0];
688 let timer = startTimer(args[0]);
689 sendConsoleAPIMessage(this, "time", frame, args, { timer });
690 dumpMessage(this, "time", "'" + timer.name + "' @ " + new Date());
693 timeEnd: function Console_timeEnd() {
694 if (!shouldLog("timeEnd", this.maxLogLevel)) {
697 let args = Array.prototype.slice.call(arguments, 0);
698 let frame = getStack(Components.stack.caller, 1)[0];
699 let timer = stopTimer(args[0]);
700 sendConsoleAPIMessage(this, "timeEnd", frame, args, { timer });
704 "'" + timer.name + "' " + timer.duration + "ms"
708 profile(profileName) {
709 if (!shouldLog("profile", this.maxLogLevel)) {
712 Services.obs.notifyObservers(
716 arguments: [profileName],
720 "console-api-profiler"
722 dumpMessage(this, "profile", `'${profileName}'`);
725 profileEnd(profileName) {
726 if (!shouldLog("profileEnd", this.maxLogLevel)) {
729 Services.obs.notifyObservers(
732 action: "profileEnd",
733 arguments: [profileName],
737 "console-api-profiler"
739 dumpMessage(this, "profileEnd", `'${profileName}'`);
743 return this._maxLogLevel || "all";
746 set maxLogLevel(aValue) {
747 this._maxLogLevel = this._maxExplicitLogLevel = aValue;
751 return shouldLog(aLevel, this.maxLogLevel);