no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / toolkit / modules / Console.sys.mjs
blob5fb4f750f41c9614e561aa486ead5f67a67eb3b7
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/. */
5 /**
6  * Define a 'console' API to roughly match the implementation provided by
7  * Firebug.
8  * This module helps cases where code is shared between the web and Firefox.
9  * See also Browser.jsm for an implementation of other web constants to help
10  * sharing code between the web and firefox;
11  *
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)
19  */
21 var gTimerRegistry = new Map();
23 /**
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.
27  *
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".
42  * @return {string}
43  *        The original string formatted to fit the specified lengths
44  */
45 function fmt(aStr, aMaxLen, aMinLen, aOptions) {
46   if (aMinLen == null) {
47     aMinLen = aMaxLen;
48   }
49   if (aStr == null) {
50     aStr = "";
51   }
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;
60     }
61     return aStr.substring(0, aMaxLen - 1) + "_";
62   }
63   if (aStr.length < aMinLen) {
64     let padding = Array(aMinLen - aStr.length + 1).join(" ");
65     aStr = aOptions.align === "end" ? padding + aStr : aStr + padding;
66   }
67   return aStr;
70 /**
71  * Utility to extract the constructor name of an object.
72  * Object.toString gives: "[object ?????]"; we want the "?????".
73  *
74  * @param {object} aObj
75  *        The object from which to extract the constructor name
76  * @return {string}
77  *        The constructor name
78  */
79 function getCtorName(aObj) {
80   if (aObj === null) {
81     return "null";
82   }
83   if (aObj === undefined) {
84     return "undefined";
85   }
86   if (aObj.constructor && aObj.constructor.name) {
87     return aObj.constructor.name;
88   }
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);
94 /**
95  * Indicates whether an object is a JS or `Components.Exception` error.
96  *
97  * @param {object} aThing
98           The object to check
99  * @return {boolean}
100           Is this object an error?
101  */
102 function isError(aThing) {
103   return (
104     aThing &&
105     ((typeof aThing.name == "string" && aThing.name.startsWith("NS_ERROR_")) ||
106       getCtorName(aThing).endsWith("Error"))
107   );
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
116  * @return {string}
117  *        A single line representation of aThing, which will generally be at
118  *        most 80 chars long
119  */
120 function stringify(aThing, aAllowNewLines) {
121   if (aThing === undefined) {
122     return "undefined";
123   }
125   if (aThing === null) {
126     return "null";
127   }
129   if (isError(aThing)) {
130     return "Message: " + aThing;
131   }
133   if (typeof aThing == "object") {
134     let type = getCtorName(aThing);
135     if (Element.isInstance(aThing)) {
136       return debugElement(aThing);
137     }
138     type = type == "Object" ? "" : type + " ";
139     let json;
140     try {
141       json = JSON.stringify(aThing);
142     } catch (ex) {
143       // Can't use a real ellipsis here, because cmd.exe isn't unicode-enabled
144       json = "{" + Object.keys(aThing).join(":..,") + ":.., }";
145     }
146     return type + json;
147   }
149   if (typeof aThing == "function") {
150     return aThing.toString().replace(/\s+/g, " ");
151   }
153   let str = aThing.toString();
154   if (!aAllowNewLines) {
155     str = str.replace(/\n/g, "|");
156   }
157   return str;
161  * Create a simple debug representation of a given element.
163  * @param {Element} aElement
164  *        The element to debug
165  * @return {string}
166  *        A simple single line representation of aElement
167  */
168 function debugElement(aElement) {
169   return (
170     "<" +
171     aElement.tagName +
172     (aElement.id ? "#" + aElement.id : "") +
173     (aElement.className && aElement.className.split
174       ? "." + aElement.className.split(" ").join(" .")
175       : "") +
176     ">"
177   );
181  * A multi line stringification of an object, designed for use by humans
183  * @param {any} aThing
184  *        The object to be stringified
185  * @return {string}
186  *        A multi line representation of aThing
187  */
188 function log(aThing) {
189   if (aThing === null) {
190     return "null\n";
191   }
193   if (aThing === undefined) {
194     return "undefined\n";
195   }
197   if (typeof aThing == "object") {
198     let reply = "";
199     let type = getCtorName(aThing);
200     if (type == "Map") {
201       reply += "Map\n";
202       for (let [key, value] of aThing) {
203         reply += logProperty(key, value);
204       }
205     } else if (type == "Set") {
206       let i = 0;
207       reply += "Set\n";
208       for (let value of aThing) {
209         reply += logProperty("" + i, value);
210         i++;
211       }
212     } else if (isError(aThing)) {
213       reply += "  Message: " + aThing + "\n";
214       if (aThing.stack) {
215         reply += "  Stack:\n";
216         var frame = aThing.stack;
217         while (frame) {
218           reply += "    " + frame + "\n";
219           frame = frame.caller;
220         }
221       }
222     } else if (Element.isInstance(aThing)) {
223       reply += "  " + debugElement(aThing) + "\n";
224     } else {
225       let keys = Object.getOwnPropertyNames(aThing);
226       if (keys.length) {
227         reply += type + "\n";
228         keys.forEach(function (aProp) {
229           reply += logProperty(aProp, aThing[aProp]);
230         });
231       } else {
232         reply += type + "\n";
233         let root = aThing;
234         let logged = [];
235         while (root != null) {
236           let properties = Object.keys(root);
237           properties.sort();
238           properties.forEach(function (property) {
239             if (!(property in logged)) {
240               logged[property] = property;
241               reply += logProperty(property, aThing[property]);
242             }
243           });
245           root = Object.getPrototypeOf(root);
246           if (root != null) {
247             reply += "  - prototype " + getCtorName(root) + "\n";
248           }
249         }
250       }
251     }
253     return reply;
254   }
256   return "  " + aThing.toString() + "\n";
260  * Helper for log() which converts a property/value pair into an output
261  * string
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
267  * @return {string}
268  *        Multi line output string describing the property/value pair
269  */
270 function logProperty(aProp, aValue) {
271   let reply = "";
272   if (aProp == "stack" && typeof value == "string") {
273     let trace = parseStack(aValue);
274     reply += formatTrace(trace);
275   } else {
276     reply += "    - " + aProp + " = " + stringify(aValue) + "\n";
277   }
278   return reply;
281 const LOG_LEVELS = {
282   all: Number.MIN_VALUE,
283   debug: 2,
284   log: 3,
285   info: 3,
286   clear: 3,
287   trace: 3,
288   timeEnd: 3,
289   time: 3,
290   assert: 3,
291   group: 3,
292   groupEnd: 3,
293   profile: 3,
294   profileEnd: 3,
295   dir: 3,
296   dirxml: 3,
297   warn: 4,
298   error: 5,
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
313  * @return {boolean}
314  *        Should this message be logged or not?
315  */
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
326  * @return {object[]}
327  *        Array of { file: "...", line: NNN, call: "..." } objects
328  */
329 function parseStack(aStack) {
330   let trace = [];
331   aStack.split("\n").forEach(function (line) {
332     if (!line) {
333       return;
334     }
335     let at = line.lastIndexOf("@");
336     let posn = line.substring(at + 1);
337     trace.push({
338       filename: posn.split(":")[0],
339       lineNumber: posn.split(":")[1],
340       functionName: line.substring(0, at),
341     });
342   });
343   return trace;
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.
354  * @return {object[]}
355  *         An array of {filename, lineNumber, functionName, language} objects.
356  *         These objects follow the same format as other ConsoleAPIStorage
357  *         messages.
358  */
359 function getStack(aFrame, aMaxDepth = 0) {
360   if (!aFrame) {
361     aFrame = Components.stack.caller;
362   }
363   let trace = [];
364   while (aFrame) {
365     trace.push({
366       filename: aFrame.filename,
367       lineNumber: aFrame.lineNumber,
368       functionName: aFrame.name,
369       language: aFrame.language,
370     });
371     if (aMaxDepth == trace.length) {
372       break;
373     }
374     aFrame = aFrame.caller;
375   }
376   return trace;
380  * Take the output from parseStack() and convert it to nice readable
381  * output
383  * @param {object[]} aTrace
384  *        Array of trace objects as created by parseStack()
385  * @return {string} Multi line report of the stack trace
386  */
387 function formatTrace(aTrace) {
388   let reply = "";
389   aTrace.forEach(function (frame) {
390     reply +=
391       fmt(frame.filename, 20, 20, { truncate: "start" }) +
392       " " +
393       fmt(frame.lineNumber, 5, 5) +
394       " " +
395       fmt(frame.functionName, 75, 0, { truncate: "center" }) +
396       "\n";
397   });
398   return reply;
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.
408  * @return {object}
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.
413  */
414 function startTimer(aName, aTimestamp) {
415   let key = aName.toString();
416   if (!gTimerRegistry.has(key)) {
417     gTimerRegistry.set(key, aTimestamp || Date.now());
418   }
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.
429  * @return {object}
430  *         The name property holds the timer name and the duration property
431  *         holds the number of milliseconds since the timer was started.
432  */
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
442  * prefix
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
450  */
451 function dumpMessage(aConsole, aLevel, aMessage) {
452   aConsole.dump(
453     "console." +
454       aLevel +
455       ": " +
456       (aConsole.prefix ? aConsole.prefix + ": " : "") +
457       aMessage +
458       "\n"
459   );
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
469  * @return {function}
470  *        A logging function
471  * @see createMultiLineDumper()
472  */
473 function createDumper(aLevel) {
474   return function () {
475     if (!shouldLog(aLevel, this.maxLogLevel)) {
476       return;
477     }
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);
483     });
484     dumpMessage(this, aLevel, data.join(" "));
485   };
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
495  * @return {function}
496  *        A logging function
497  * @see createDumper()
498  */
499 function createMultiLineDumper(aLevel) {
500   return function () {
501     if (!shouldLog(aLevel, this.maxLogLevel)) {
502       return;
503     }
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) {
509       this.dump(log(arg));
510     }, this);
511   };
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
522  *        that was called.
523  * @param {object} aFrame
524  *        The youngest stack frame coming from Components.stack, as formatted by
525  *        getStack().
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
533  *        getStack().
534  */
535 function sendConsoleAPIMessage(aConsole, aLevel, aFrame, aArgs, aOptions = {}) {
536   let consoleEvent = {
537     ID: "jsm",
538     innerID: aConsole.innerID || aFrame.filename,
539     consoleID: aConsole.consoleID,
540     level: aLevel,
541     filename: aFrame.filename,
542     lineNumber: aFrame.lineNumber,
543     functionName: aFrame.functionName,
544     timeStamp: Date.now(),
545     arguments: aArgs,
546     prefix: aConsole.prefix,
547     chromeContext: true,
548   };
550   consoleEvent.wrappedJSObject = consoleEvent;
552   switch (aLevel) {
553     case "trace":
554       consoleEvent.stacktrace = aOptions.stacktrace;
555       break;
556     case "time":
557     case "timeEnd":
558       consoleEvent.timer = aOptions.timer;
559       break;
560     case "group":
561     case "groupCollapsed":
562     case "groupEnd":
563       try {
564         consoleEvent.groupName = Array.prototype.join.call(aArgs, " ");
565       } catch (ex) {
566         console.error(ex);
567         console.error(ex.stack);
568         return;
569       }
570       break;
571   }
573   let ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(
574     Ci.nsIConsoleAPIStorage
575   );
576   if (ConsoleAPIStorage) {
577     ConsoleAPIStorage.recordEvent("jsm", consoleEvent);
578   }
582  * This creates a console object that somewhat replicates Firebug's console
583  * object
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
601  *                            written to stdout
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
606  * @return {object}
607  *        A console API instance object
608  */
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 = () => {
620     if (
621       Services.prefs.getPrefType(aConsoleOptions.maxLogLevelPref) ==
622       Services.prefs.PREF_STRING
623     ) {
624       this._maxLogLevel = Services.prefs
625         .getCharPref(aConsoleOptions.maxLogLevelPref)
626         .toLowerCase();
627     } else {
628       this._maxLogLevel = this._maxExplicitLogLevel;
629     }
630   };
632   if (aConsoleOptions.maxLogLevelPref) {
633     updateMaxLogLevel();
634     Services.prefs.addObserver(
635       aConsoleOptions.maxLogLevelPref,
636       updateMaxLogLevel
637     );
638   }
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);
644     }
645   }
648 ConsoleAPI.prototype = {
649   /**
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.
652    */
653   _maxExplicitLogLevel: null,
654   /**
655    * The current log level via all methods of setting (pref or via the API).
656    */
657   _maxLogLevel: null,
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)) {
668       return;
669     }
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));
674   },
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)) {
684       return;
685     }
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());
691   },
693   timeEnd: function Console_timeEnd() {
694     if (!shouldLog("timeEnd", this.maxLogLevel)) {
695       return;
696     }
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 });
701     dumpMessage(
702       this,
703       "timeEnd",
704       "'" + timer.name + "' " + timer.duration + "ms"
705     );
706   },
708   profile(profileName) {
709     if (!shouldLog("profile", this.maxLogLevel)) {
710       return;
711     }
712     Services.obs.notifyObservers(
713       {
714         wrappedJSObject: {
715           action: "profile",
716           arguments: [profileName],
717           chromeContext: true,
718         },
719       },
720       "console-api-profiler"
721     );
722     dumpMessage(this, "profile", `'${profileName}'`);
723   },
725   profileEnd(profileName) {
726     if (!shouldLog("profileEnd", this.maxLogLevel)) {
727       return;
728     }
729     Services.obs.notifyObservers(
730       {
731         wrappedJSObject: {
732           action: "profileEnd",
733           arguments: [profileName],
734           chromeContext: true,
735         },
736       },
737       "console-api-profiler"
738     );
739     dumpMessage(this, "profileEnd", `'${profileName}'`);
740   },
742   get maxLogLevel() {
743     return this._maxLogLevel || "all";
744   },
746   set maxLogLevel(aValue) {
747     this._maxLogLevel = this._maxExplicitLogLevel = aValue;
748   },
750   shouldLog(aLevel) {
751     return shouldLog(aLevel, this.maxLogLevel);
752   },