lib/goog/debug/debug.js

1// Copyright 2006 The Closure Library Authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS-IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/**
16 * @fileoverview Logging and debugging utilities.
17 *
18 * @see ../demos/debug.html
19 */
20
21goog.provide('goog.debug');
22
23goog.require('goog.array');
24goog.require('goog.html.SafeHtml');
25goog.require('goog.html.SafeUrl');
26goog.require('goog.html.uncheckedconversions');
27goog.require('goog.string.Const');
28goog.require('goog.structs.Set');
29goog.require('goog.userAgent');
30
31
32/** @define {boolean} Whether logging should be enabled. */
33goog.define('goog.debug.LOGGING_ENABLED', goog.DEBUG);
34
35
36/**
37 * Catches onerror events fired by windows and similar objects.
38 * @param {function(Object)} logFunc The function to call with the error
39 * information.
40 * @param {boolean=} opt_cancel Whether to stop the error from reaching the
41 * browser.
42 * @param {Object=} opt_target Object that fires onerror events.
43 */
44goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) {
45 var target = opt_target || goog.global;
46 var oldErrorHandler = target.onerror;
47 var retVal = !!opt_cancel;
48
49 // Chrome interprets onerror return value backwards (http://crbug.com/92062)
50 // until it was fixed in webkit revision r94061 (Webkit 535.3). This
51 // workaround still needs to be skipped in Safari after the webkit change
52 // gets pushed out in Safari.
53 // See https://bugs.webkit.org/show_bug.cgi?id=67119
54 if (goog.userAgent.WEBKIT &&
55 !goog.userAgent.isVersionOrHigher('535.3')) {
56 retVal = !retVal;
57 }
58
59 /**
60 * New onerror handler for this target. This onerror handler follows the spec
61 * according to
62 * http://www.whatwg.org/specs/web-apps/current-work/#runtime-script-errors
63 * The spec was changed in August 2013 to support receiving column information
64 * and an error object for all scripts on the same origin or cross origin
65 * scripts with the proper headers. See
66 * https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
67 *
68 * @param {string} message The error message. For cross-origin errors, this
69 * will be scrubbed to just "Script error.". For new browsers that have
70 * updated to follow the latest spec, errors that come from origins that
71 * have proper cross origin headers will not be scrubbed.
72 * @param {string} url The URL of the script that caused the error. The URL
73 * will be scrubbed to "" for cross origin scripts unless the script has
74 * proper cross origin headers and the browser has updated to the latest
75 * spec.
76 * @param {number} line The line number in the script that the error
77 * occurred on.
78 * @param {number=} opt_col The optional column number that the error
79 * occurred on. Only browsers that have updated to the latest spec will
80 * include this.
81 * @param {Error=} opt_error The optional actual error object for this
82 * error that should include the stack. Only browsers that have updated
83 * to the latest spec will inlude this parameter.
84 * @return {boolean} Whether to prevent the error from reaching the browser.
85 */
86 target.onerror = function(message, url, line, opt_col, opt_error) {
87 if (oldErrorHandler) {
88 oldErrorHandler(message, url, line, opt_col, opt_error);
89 }
90 logFunc({
91 message: message,
92 fileName: url,
93 line: line,
94 col: opt_col,
95 error: opt_error
96 });
97 return retVal;
98 };
99};
100
101
102/**
103 * Creates a string representing an object and all its properties.
104 * @param {Object|null|undefined} obj Object to expose.
105 * @param {boolean=} opt_showFn Show the functions as well as the properties,
106 * default is false.
107 * @return {string} The string representation of {@code obj}.
108 */
109goog.debug.expose = function(obj, opt_showFn) {
110 if (typeof obj == 'undefined') {
111 return 'undefined';
112 }
113 if (obj == null) {
114 return 'NULL';
115 }
116 var str = [];
117
118 for (var x in obj) {
119 if (!opt_showFn && goog.isFunction(obj[x])) {
120 continue;
121 }
122 var s = x + ' = ';
123 /** @preserveTry */
124 try {
125 s += obj[x];
126 } catch (e) {
127 s += '*** ' + e + ' ***';
128 }
129 str.push(s);
130 }
131 return str.join('\n');
132};
133
134
135/**
136 * Creates a string representing a given primitive or object, and for an
137 * object, all its properties and nested objects. WARNING: If an object is
138 * given, it and all its nested objects will be modified. To detect reference
139 * cycles, this method identifies objects using goog.getUid() which mutates the
140 * object.
141 * @param {*} obj Object to expose.
142 * @param {boolean=} opt_showFn Also show properties that are functions (by
143 * default, functions are omitted).
144 * @return {string} A string representation of {@code obj}.
145 */
146goog.debug.deepExpose = function(obj, opt_showFn) {
147 var str = [];
148
149 var helper = function(obj, space, parentSeen) {
150 var nestspace = space + ' ';
151 var seen = new goog.structs.Set(parentSeen);
152
153 var indentMultiline = function(str) {
154 return str.replace(/\n/g, '\n' + space);
155 };
156
157 /** @preserveTry */
158 try {
159 if (!goog.isDef(obj)) {
160 str.push('undefined');
161 } else if (goog.isNull(obj)) {
162 str.push('NULL');
163 } else if (goog.isString(obj)) {
164 str.push('"' + indentMultiline(obj) + '"');
165 } else if (goog.isFunction(obj)) {
166 str.push(indentMultiline(String(obj)));
167 } else if (goog.isObject(obj)) {
168 if (seen.contains(obj)) {
169 str.push('*** reference loop detected ***');
170 } else {
171 seen.add(obj);
172 str.push('{');
173 for (var x in obj) {
174 if (!opt_showFn && goog.isFunction(obj[x])) {
175 continue;
176 }
177 str.push('\n');
178 str.push(nestspace);
179 str.push(x + ' = ');
180 helper(obj[x], nestspace, seen);
181 }
182 str.push('\n' + space + '}');
183 }
184 } else {
185 str.push(obj);
186 }
187 } catch (e) {
188 str.push('*** ' + e + ' ***');
189 }
190 };
191
192 helper(obj, '', new goog.structs.Set());
193 return str.join('');
194};
195
196
197/**
198 * Recursively outputs a nested array as a string.
199 * @param {Array<?>} arr The array.
200 * @return {string} String representing nested array.
201 */
202goog.debug.exposeArray = function(arr) {
203 var str = [];
204 for (var i = 0; i < arr.length; i++) {
205 if (goog.isArray(arr[i])) {
206 str.push(goog.debug.exposeArray(arr[i]));
207 } else {
208 str.push(arr[i]);
209 }
210 }
211 return '[ ' + str.join(', ') + ' ]';
212};
213
214
215/**
216 * Exposes an exception that has been caught by a try...catch and outputs the
217 * error as HTML with a stack trace.
218 * @param {Object} err Error object or string.
219 * @param {Function=} opt_fn Optional function to start stack trace from.
220 * @return {string} Details of exception, as HTML.
221 */
222goog.debug.exposeException = function(err, opt_fn) {
223 var html = goog.debug.exposeExceptionAsHtml(err, opt_fn);
224 return goog.html.SafeHtml.unwrap(html);
225};
226
227
228/**
229 * Exposes an exception that has been caught by a try...catch and outputs the
230 * error with a stack trace.
231 * @param {Object} err Error object or string.
232 * @param {Function=} opt_fn Optional function to start stack trace from.
233 * @return {!goog.html.SafeHtml} Details of exception.
234 */
235goog.debug.exposeExceptionAsHtml = function(err, opt_fn) {
236 /** @preserveTry */
237 try {
238 var e = goog.debug.normalizeErrorObject(err);
239 // Create the error message
240 var viewSourceUrl = goog.debug.createViewSourceUrl_(e.fileName);
241 var error = goog.html.SafeHtml.concat(
242 goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
243 'Message: ' + e.message + '\nUrl: '),
244 goog.html.SafeHtml.create('a',
245 {href: viewSourceUrl, target: '_new'}, e.fileName),
246 goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
247 '\nLine: ' + e.lineNumber + '\n\nBrowser stack:\n' +
248 e.stack + '-> ' + '[end]\n\nJS stack traversal:\n' +
249 goog.debug.getStacktrace(opt_fn) + '-> '));
250 return error;
251 } catch (e2) {
252 return goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
253 'Exception trying to expose exception! You win, we lose. ' + e2);
254 }
255};
256
257
258/**
259 * @param {?string=} opt_fileName
260 * @return {!goog.html.SafeUrl} SafeUrl with view-source scheme, pointing at
261 * fileName.
262 * @private
263 */
264goog.debug.createViewSourceUrl_ = function(opt_fileName) {
265 if (!goog.isDefAndNotNull(opt_fileName)) {
266 opt_fileName = '';
267 }
268 if (!/^https?:\/\//i.test(opt_fileName)) {
269 return goog.html.SafeUrl.fromConstant(
270 goog.string.Const.from('sanitizedviewsrc'));
271 }
272 var sanitizedFileName = goog.html.SafeUrl.sanitize(opt_fileName);
273 return goog.html.uncheckedconversions.
274 safeUrlFromStringKnownToSatisfyTypeContract(
275 goog.string.Const.from('view-source scheme plus HTTP/HTTPS URL'),
276 'view-source:' + goog.html.SafeUrl.unwrap(sanitizedFileName));
277};
278
279
280/**
281 * Normalizes the error/exception object between browsers.
282 * @param {Object} err Raw error object.
283 * @return {!Object} Normalized error object.
284 */
285goog.debug.normalizeErrorObject = function(err) {
286 var href = goog.getObjectByName('window.location.href');
287 if (goog.isString(err)) {
288 return {
289 'message': err,
290 'name': 'Unknown error',
291 'lineNumber': 'Not available',
292 'fileName': href,
293 'stack': 'Not available'
294 };
295 }
296
297 var lineNumber, fileName;
298 var threwError = false;
299
300 try {
301 lineNumber = err.lineNumber || err.line || 'Not available';
302 } catch (e) {
303 // Firefox 2 sometimes throws an error when accessing 'lineNumber':
304 // Message: Permission denied to get property UnnamedClass.lineNumber
305 lineNumber = 'Not available';
306 threwError = true;
307 }
308
309 try {
310 fileName = err.fileName || err.filename || err.sourceURL ||
311 // $googDebugFname may be set before a call to eval to set the filename
312 // that the eval is supposed to present.
313 goog.global['$googDebugFname'] || href;
314 } catch (e) {
315 // Firefox 2 may also throw an error when accessing 'filename'.
316 fileName = 'Not available';
317 threwError = true;
318 }
319
320 // The IE Error object contains only the name and the message.
321 // The Safari Error object uses the line and sourceURL fields.
322 if (threwError || !err.lineNumber || !err.fileName || !err.stack ||
323 !err.message || !err.name) {
324 return {
325 'message': err.message || 'Not available',
326 'name': err.name || 'UnknownError',
327 'lineNumber': lineNumber,
328 'fileName': fileName,
329 'stack': err.stack || 'Not available'
330 };
331 }
332
333 // Standards error object
334 return err;
335};
336
337
338/**
339 * Converts an object to an Error if it's a String,
340 * adds a stacktrace if there isn't one,
341 * and optionally adds an extra message.
342 * @param {Error|string} err the original thrown object or string.
343 * @param {string=} opt_message optional additional message to add to the
344 * error.
345 * @return {!Error} If err is a string, it is used to create a new Error,
346 * which is enhanced and returned. Otherwise err itself is enhanced
347 * and returned.
348 */
349goog.debug.enhanceError = function(err, opt_message) {
350 var error;
351 if (typeof err == 'string') {
352 error = Error(err);
353 if (Error.captureStackTrace) {
354 // Trim this function off the call stack, if we can.
355 Error.captureStackTrace(error, goog.debug.enhanceError);
356 }
357 } else {
358 error = err;
359 }
360
361 if (!error.stack) {
362 error.stack = goog.debug.getStacktrace(goog.debug.enhanceError);
363 }
364 if (opt_message) {
365 // find the first unoccupied 'messageX' property
366 var x = 0;
367 while (error['message' + x]) {
368 ++x;
369 }
370 error['message' + x] = String(opt_message);
371 }
372 return error;
373};
374
375
376/**
377 * Gets the current stack trace. Simple and iterative - doesn't worry about
378 * catching circular references or getting the args.
379 * @param {number=} opt_depth Optional maximum depth to trace back to.
380 * @return {string} A string with the function names of all functions in the
381 * stack, separated by \n.
382 * @suppress {es5Strict}
383 */
384goog.debug.getStacktraceSimple = function(opt_depth) {
385 if (goog.STRICT_MODE_COMPATIBLE) {
386 var stack = goog.debug.getNativeStackTrace_(goog.debug.getStacktraceSimple);
387 if (stack) {
388 return stack;
389 }
390 // NOTE: browsers that have strict mode support also have native "stack"
391 // properties. Fall-through for legacy browser support.
392 }
393
394 var sb = [];
395 var fn = arguments.callee.caller;
396 var depth = 0;
397
398 while (fn && (!opt_depth || depth < opt_depth)) {
399 sb.push(goog.debug.getFunctionName(fn));
400 sb.push('()\n');
401 /** @preserveTry */
402 try {
403 fn = fn.caller;
404 } catch (e) {
405 sb.push('[exception trying to get caller]\n');
406 break;
407 }
408 depth++;
409 if (depth >= goog.debug.MAX_STACK_DEPTH) {
410 sb.push('[...long stack...]');
411 break;
412 }
413 }
414 if (opt_depth && depth >= opt_depth) {
415 sb.push('[...reached max depth limit...]');
416 } else {
417 sb.push('[end]');
418 }
419
420 return sb.join('');
421};
422
423
424/**
425 * Max length of stack to try and output
426 * @type {number}
427 */
428goog.debug.MAX_STACK_DEPTH = 50;
429
430
431/**
432 * @param {Function} fn The function to start getting the trace from.
433 * @return {?string}
434 * @private
435 */
436goog.debug.getNativeStackTrace_ = function(fn) {
437 var tempErr = new Error();
438 if (Error.captureStackTrace) {
439 Error.captureStackTrace(tempErr, fn);
440 return String(tempErr.stack);
441 } else {
442 // IE10, only adds stack traces when an exception is thrown.
443 try {
444 throw tempErr;
445 } catch (e) {
446 tempErr = e;
447 }
448 var stack = tempErr.stack;
449 if (stack) {
450 return String(stack);
451 }
452 }
453 return null;
454};
455
456
457/**
458 * Gets the current stack trace, either starting from the caller or starting
459 * from a specified function that's currently on the call stack.
460 * @param {Function=} opt_fn Optional function to start getting the trace from.
461 * If not provided, defaults to the function that called this.
462 * @return {string} Stack trace.
463 * @suppress {es5Strict}
464 */
465goog.debug.getStacktrace = function(opt_fn) {
466 var stack;
467 if (goog.STRICT_MODE_COMPATIBLE) {
468 // Try to get the stack trace from the environment if it is available.
469 var contextFn = opt_fn || goog.debug.getStacktrace;
470 stack = goog.debug.getNativeStackTrace_(contextFn);
471 }
472 if (!stack) {
473 // NOTE: browsers that have strict mode support also have native "stack"
474 // properties. This function will throw in strict mode.
475 stack = goog.debug.getStacktraceHelper_(
476 opt_fn || arguments.callee.caller, []);
477 }
478 return stack;
479};
480
481
482/**
483 * Private helper for getStacktrace().
484 * @param {Function} fn Function to start getting the trace from.
485 * @param {Array<!Function>} visited List of functions visited so far.
486 * @return {string} Stack trace starting from function fn.
487 * @suppress {es5Strict}
488 * @private
489 */
490goog.debug.getStacktraceHelper_ = function(fn, visited) {
491 var sb = [];
492
493 // Circular reference, certain functions like bind seem to cause a recursive
494 // loop so we need to catch circular references
495 if (goog.array.contains(visited, fn)) {
496 sb.push('[...circular reference...]');
497
498 // Traverse the call stack until function not found or max depth is reached
499 } else if (fn && visited.length < goog.debug.MAX_STACK_DEPTH) {
500 sb.push(goog.debug.getFunctionName(fn) + '(');
501 var args = fn.arguments;
502 // Args may be null for some special functions such as host objects or eval.
503 for (var i = 0; args && i < args.length; i++) {
504 if (i > 0) {
505 sb.push(', ');
506 }
507 var argDesc;
508 var arg = args[i];
509 switch (typeof arg) {
510 case 'object':
511 argDesc = arg ? 'object' : 'null';
512 break;
513
514 case 'string':
515 argDesc = arg;
516 break;
517
518 case 'number':
519 argDesc = String(arg);
520 break;
521
522 case 'boolean':
523 argDesc = arg ? 'true' : 'false';
524 break;
525
526 case 'function':
527 argDesc = goog.debug.getFunctionName(arg);
528 argDesc = argDesc ? argDesc : '[fn]';
529 break;
530
531 case 'undefined':
532 default:
533 argDesc = typeof arg;
534 break;
535 }
536
537 if (argDesc.length > 40) {
538 argDesc = argDesc.substr(0, 40) + '...';
539 }
540 sb.push(argDesc);
541 }
542 visited.push(fn);
543 sb.push(')\n');
544 /** @preserveTry */
545 try {
546 sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited));
547 } catch (e) {
548 sb.push('[exception trying to get caller]\n');
549 }
550
551 } else if (fn) {
552 sb.push('[...long stack...]');
553 } else {
554 sb.push('[end]');
555 }
556 return sb.join('');
557};
558
559
560/**
561 * Set a custom function name resolver.
562 * @param {function(Function): string} resolver Resolves functions to their
563 * names.
564 */
565goog.debug.setFunctionResolver = function(resolver) {
566 goog.debug.fnNameResolver_ = resolver;
567};
568
569
570/**
571 * Gets a function name
572 * @param {Function} fn Function to get name of.
573 * @return {string} Function's name.
574 */
575goog.debug.getFunctionName = function(fn) {
576 if (goog.debug.fnNameCache_[fn]) {
577 return goog.debug.fnNameCache_[fn];
578 }
579 if (goog.debug.fnNameResolver_) {
580 var name = goog.debug.fnNameResolver_(fn);
581 if (name) {
582 goog.debug.fnNameCache_[fn] = name;
583 return name;
584 }
585 }
586
587 // Heuristically determine function name based on code.
588 var functionSource = String(fn);
589 if (!goog.debug.fnNameCache_[functionSource]) {
590 var matches = /function ([^\(]+)/.exec(functionSource);
591 if (matches) {
592 var method = matches[1];
593 goog.debug.fnNameCache_[functionSource] = method;
594 } else {
595 goog.debug.fnNameCache_[functionSource] = '[Anonymous]';
596 }
597 }
598
599 return goog.debug.fnNameCache_[functionSource];
600};
601
602
603/**
604 * Makes whitespace visible by replacing it with printable characters.
605 * This is useful in finding diffrences between the expected and the actual
606 * output strings of a testcase.
607 * @param {string} string whose whitespace needs to be made visible.
608 * @return {string} string whose whitespace is made visible.
609 */
610goog.debug.makeWhitespaceVisible = function(string) {
611 return string.replace(/ /g, '[_]')
612 .replace(/\f/g, '[f]')
613 .replace(/\n/g, '[n]\n')
614 .replace(/\r/g, '[r]')
615 .replace(/\t/g, '[t]');
616};
617
618
619/**
620 * Returns the type of a value. If a constructor is passed, and a suitable
621 * string cannot be found, 'unknown type name' will be returned.
622 *
623 * <p>Forked rather than moved from {@link goog.asserts.getType_}
624 * to avoid adding a dependency to goog.asserts.
625 * @param {*} value A constructor, object, or primitive.
626 * @return {string} The best display name for the value, or 'unknown type name'.
627 */
628goog.debug.runtimeType = function(value) {
629 if (value instanceof Function) {
630 return value.displayName || value.name || 'unknown type name';
631 } else if (value instanceof Object) {
632 return value.constructor.displayName || value.constructor.name ||
633 Object.prototype.toString.call(value);
634 } else {
635 return value === null ? 'null' : typeof value;
636 }
637};
638
639
640/**
641 * Hash map for storing function names that have already been looked up.
642 * @type {Object}
643 * @private
644 */
645goog.debug.fnNameCache_ = {};
646
647
648/**
649 * Resolves functions to their names. Resolved function names will be cached.
650 * @type {function(Function):string}
651 * @private
652 */
653goog.debug.fnNameResolver_;