1// Domain Public by Eric Wendelin http://eriwen.com/ (2008) 2// Luke Smith http://lucassmith.name/ (2008) 3// Loic Dachary <[email protected]> (2008) 4// Johan Euphrosine <[email protected]> (2008) 5// Oyvind Sean Kinsey http://kinsey.no/blog (2010) 6// Victor Homyakov <[email protected]> (2010) 7/*global module, exports, define, ActiveXObject*/ 8(function(global, factory) { 9 if (typeof exports === 'object') { 10 // Node 11 module.exports = factory(); 12 } else if (typeof define === 'function' && define.amd) { 13 // AMD 14 define(factory); 15 } else { 16 // Browser globals 17 global.printStackTrace = factory(); 18 } 19}(this, function() { 20 /** 21 * Main function giving a function stack trace with a forced or passed in Error 22 * 23 * @cfg {Error} e The error to create a stacktrace from (optional) 24 * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions 25 * @return {Array} of Strings with functions, lines, files, and arguments where possible 26 */ 27 function printStackTrace(options) { 28 options = options || {guess: true}; 29 var ex = options.e || null, guess = !!options.guess; 30 var p = new printStackTrace.implementation(), result = p.run(ex); 31 return (guess) ? p.guessAnonymousFunctions(result) : result; 32 } 33 34 printStackTrace.implementation = function() { 35 }; 36 37 printStackTrace.implementation.prototype = { 38 /** 39 * @param {Error} ex The error to create a stacktrace from (optional) 40 * @param {String} mode Forced mode (optional, mostly for unit tests) 41 */ 42 run: function(ex, mode) { 43 ex = ex || this.createException(); 44 // examine exception properties w/o debugger 45 //for (var prop in ex) {alert("Ex['" + prop + "']=" + ex[prop]);} 46 mode = mode || this.mode(ex); 47 if (mode === 'other') { 48 return this.other(arguments.callee); 49 } else { 50 return this[mode](ex); 51 } 52 }, 53 54 createException: function() { 55 try { 56 this.undef(); 57 } catch (e) { 58 return e; 59 } 60 }, 61 62 /** 63 * Mode could differ for different exception, e.g. 64 * exceptions in Chrome may or may not have arguments or stack. 65 * 66 * @return {String} mode of operation for the exception 67 */ 68 mode: function(e) { 69 if (e['arguments'] && e.stack) { 70 return 'chrome'; 71 } else if (e.stack && e.sourceURL) { 72 return 'safari'; 73 } else if (e.stack && e.number) { 74 return 'ie'; 75 } else if (e.stack && e.fileName) { 76 return 'firefox'; 77 } else if (e.message && e['opera#sourceloc']) { 78 // e.message.indexOf("Backtrace:") > -1 -> opera9 79 // 'opera#sourceloc' in e -> opera9, opera10a 80 // !e.stacktrace -> opera9 81 if (!e.stacktrace) { 82 return 'opera9'; // use e.message 83 } 84 if (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length) { 85 // e.message may have more stack entries than e.stacktrace 86 return 'opera9'; // use e.message 87 } 88 return 'opera10a'; // use e.stacktrace 89 } else if (e.message && e.stack && e.stacktrace) { 90 // e.stacktrace && e.stack -> opera10b 91 if (e.stacktrace.indexOf("called from line") < 0) { 92 return 'opera10b'; // use e.stacktrace, format differs from 'opera10a' 93 } 94 // e.stacktrace && e.stack -> opera11 95 return 'opera11'; // use e.stacktrace, format differs from 'opera10a', 'opera10b' 96 } else if (e.stack && !e.fileName) { 97 // Chrome 27 does not have e.arguments as earlier versions, 98 // but still does not have e.fileName as Firefox 99 return 'chrome'; 100 } 101 return 'other'; 102 }, 103 104 /** 105 * Given a context, function name, and callback function, overwrite it so that it calls 106 * printStackTrace() first with a callback and then runs the rest of the body. 107 * 108 * @param {Object} context of execution (e.g. window) 109 * @param {String} functionName to instrument 110 * @param {Function} callback function to call with a stack trace on invocation 111 */ 112 instrumentFunction: function(context, functionName, callback) { 113 context = context || window; 114 var original = context[functionName]; 115 context[functionName] = function instrumented() { 116 callback.call(this, printStackTrace().slice(4)); 117 return context[functionName]._instrumented.apply(this, arguments); 118 }; 119 context[functionName]._instrumented = original; 120 }, 121 122 /** 123 * Given a context and function name of a function that has been 124 * instrumented, revert the function to it's original (non-instrumented) 125 * state. 126 * 127 * @param {Object} context of execution (e.g. window) 128 * @param {String} functionName to de-instrument 129 */ 130 deinstrumentFunction: function(context, functionName) { 131 if (context[functionName].constructor === Function && 132 context[functionName]._instrumented && 133 context[functionName]._instrumented.constructor === Function) { 134 context[functionName] = context[functionName]._instrumented; 135 } 136 }, 137 138 /** 139 * Given an Error object, return a formatted Array based on Chrome's stack string. 140 * 141 * @param e - Error object to inspect 142 * @return Array<String> of function calls, files and line numbers 143 */ 144 chrome: function(e) { 145 return (e.stack + '\n') 146 .replace(/^[\s\S]+?\s+at\s+/, ' at ') // remove message
147 .replace(/^\s+(at eval )?at\s+/gm, '') // remove 'at' and indentation 148 .replace(/^([^\(]+?)([\n$])/gm, '{anonymous}() ($1)$2') 149 .replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}() ($1)') 150 .replace(/^(.+) \((.+)\)$/gm, '$1@$2') 151 .split('\n') 152 .slice(0, -1); 153 }, 154 155 /** 156 * Given an Error object, return a formatted Array based on Safari's stack string. 157 * 158 * @param e - Error object to inspect 159 * @return Array<String> of function calls, files and line numbers 160 */ 161 safari: function(e) { 162 return e.stack.replace(/\[native code\]\n/m, '') 163 .replace(/^(?=\w+Error\:).*$\n/m, '') 164 .replace(/^@/gm, '{anonymous}()@') 165 .split('\n'); 166 }, 167 168 /** 169 * Given an Error object, return a formatted Array based on IE's stack string. 170 * 171 * @param e - Error object to inspect 172 * @return Array<String> of function calls, files and line numbers 173 */ 174 ie: function(e) { 175 return e.stack 176 .replace(/^\s*at\s+(.*)$/gm, '$1') 177 .replace(/^Anonymous function\s+/gm, '{anonymous}() ') 178 .replace(/^(.+)\s+\((.+)\)$/gm, '$1@$2') 179 .split('\n') 180 .slice(1); 181 }, 182 183 /** 184 * Given an Error object, return a formatted Array based on Firefox's stack string. 185 * 186 * @param e - Error object to inspect 187 * @return Array<String> of function calls, files and line numbers 188 */ 189 firefox: function(e) { 190 return e.stack.replace(/(?:\n@:0)?\s+$/m, '') 191 .replace(/^(?:\((\S*)\))?@/gm, '{anonymous}($1)@') 192 .split('\n'); 193 }, 194 195 opera11: function(e) { 196 var ANON = '{anonymous}', lineRE = /^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/; 197 var lines = e.stacktrace.split('\n'), result = []; 198 199 for (var i = 0, len = lines.length; i < len; i += 2) { 200 var match = lineRE.exec(lines[i]); 201 if (match) { 202 var location = match[4] + ':' + match[1] + ':' + match[2]; 203 var fnName = match[3] || "global code"; 204 fnName = fnName.replace(/<anonymous function: (\S+)>/, "$1").replace(/<anonymous function>/, ANON); 205 result.push(fnName + '@' + location + ' -- ' + lines[i + 1].replace(/^\s+/, '')); 206 } 207 } 208 209 return result; 210 }, 211 212 opera10b: function(e) { 213 // "<anonymous function: run>([arguments not available])@file://localhost/G:/js/stacktrace.js:27\n" + 214 // "printStackTrace([arguments not available])@file://localhost/G:/js/stacktrace.js:18\n" + 215 // "@file://localhost/G:/js/test/functional/testcase1.html:15" 216 var lineRE = /^(.*)@(.+):(\d+)$/; 217 var lines = e.stacktrace.split('\n'), result = []; 218 219 for (var i = 0, len = lines.length; i < len; i++) { 220 var match = lineRE.exec(lines[i]); 221 if (match) { 222 var fnName = match[1] ? (match[1] + '()') : "global code"; 223 result.push(fnName + '@' + match[2] + ':' + match[3]); 224 } 225 } 226 227 return result; 228 }, 229 230 /** 231 * Given an Error object, return a formatted Array based on Opera 10's stacktrace string. 232 * 233 * @param e - Error object to inspect 234 * @return Array<String> of function calls, files and line numbers 235 */ 236 opera10a: function(e) { 237 // " Line 27 of linked script file://localhost/G:/js/stacktrace.js\n" 238 // " Line 11 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html: In function foo\n" 239 var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; 240 var lines = e.stacktrace.split('\n'), result = []; 241 242 for (var i = 0, len = lines.length; i < len; i += 2) { 243 var match = lineRE.exec(lines[i]); 244 if (match) { 245 var fnName = match[3] || ANON; 246 result.push(fnName + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, '')); 247 } 248 } 249 250 return result; 251 }, 252 253 // Opera 7.x-9.2x only! 254 opera9: function(e) { 255 // " Line 43 of linked script file://localhost/G:/js/stacktrace.js\n" 256 // " Line 7 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html\n" 257 var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)/i; 258 var lines = e.message.split('\n'), result = []; 259 260 for (var i = 2, len = lines.length; i < len; i += 2) { 261 var match = lineRE.exec(lines[i]); 262 if (match) { 263 result.push(ANON + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, '')); 264 } 265 } 266 267 return result; 268 }, 269 270 // Safari 5-, IE 9-, and others 271 other: function(curr) { 272 var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], fn, args, maxStackSize = 10; 273 while (curr && curr['arguments'] && stack.length < maxStackSize) { 274 fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON; 275 args = Array.prototype.slice.call(curr['arguments'] || []); 276 stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')'; 277 curr = curr.caller; 278 } 279 return stack; 280 }, 281 282 /** 283 * Given arguments array as a String, substituting type names for non-string types. 284 * 285 * @param {Arguments,Array} args 286 * @return {String} stringified arguments 287 */
288 stringifyArguments: function(args) { 289 var result = []; 290 var slice = Array.prototype.slice; 291 for (var i = 0; i < args.length; ++i) { 292 var arg = args[i]; 293 if (arg === undefined) { 294 result[i] = 'undefined'; 295 } else if (arg === null) { 296 result[i] = 'null'; 297 } else if (arg.constructor) { 298 if (arg.constructor === Array) { 299 if (arg.length < 3) { 300 result[i] = '[' + this.stringifyArguments(arg) + ']'; 301 } else { 302 result[i] = '[' + this.stringifyArguments(slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(slice.call(arg, -1)) + ']'; 303 } 304 } else if (arg.constructor === Object) { 305 result[i] = '#object'; 306 } else if (arg.constructor === Function) { 307 result[i] = '#function'; 308 } else if (arg.constructor === String) { 309 result[i] = '"' + arg + '"'; 310 } else if (arg.constructor === Number) { 311 result[i] = arg; 312 } 313 } 314 } 315 return result.join(','); 316 }, 317 318 sourceCache: {}, 319 320 /** 321 * @return the text from a given URL 322 */ 323 ajax: function(url) { 324 var req = this.createXMLHTTPObject(); 325 if (req) { 326 try { 327 req.open('GET', url, false); 328 //req.overrideMimeType('text/plain'); 329 //req.overrideMimeType('text/javascript'); 330 req.send(null); 331 //return req.status == 200 ? req.responseText : ''; 332 return req.responseText; 333 } catch (e) { 334 } 335 } 336 return ''; 337 }, 338 339 /** 340 * Try XHR methods in order and store XHR factory. 341 * 342 * @return <Function> XHR function or equivalent 343 */ 344 createXMLHTTPObject: function() { 345 var xmlhttp, XMLHttpFactories = [ 346 function() { 347 return new XMLHttpRequest(); 348 }, function() { 349 return new ActiveXObject('Msxml2.XMLHTTP'); 350 }, function() { 351 return new ActiveXObject('Msxml3.XMLHTTP'); 352 }, function() { 353 return new ActiveXObject('Microsoft.XMLHTTP'); 354 } 355 ]; 356 for (var i = 0; i < XMLHttpFactories.length; i++) { 357 try { 358 xmlhttp = XMLHttpFactories[i](); 359 // Use memoization to cache the factory 360 this.createXMLHTTPObject = XMLHttpFactories[i]; 361 return xmlhttp; 362 } catch (e) { 363 } 364 } 365 }, 366 367 /** 368 * Given a URL, check if it is in the same domain (so we can get the source 369 * via Ajax). 370 * 371 * @param url <String> source url 372 * @return <Boolean> False if we need a cross-domain request 373 */ 374 isSameDomain: function(url) { 375 return typeof location !== "undefined" && url.indexOf(location.hostname) !== -1; // location may not be defined, e.g. when running from nodejs. 376 }, 377 378 /** 379 * Get source code from given URL if in the same domain. 380 * 381 * @param url <String> JS source URL 382 * @return <Array> Array of source code lines 383 */ 384 getSource: function(url) { 385 // TODO reuse source from script tags? 386 if (!(url in this.sourceCache)) { 387 this.sourceCache[url] = this.ajax(url).split('\n'); 388 } 389 return this.sourceCache[url]; 390 }, 391 392 guessAnonymousFunctions: function(stack) { 393 for (var i = 0; i < stack.length; ++i) { 394 var reStack = /\{anonymous\}\(.*\)@(.*)/, 395 reRef = /^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/, 396 frame = stack[i], ref = reStack.exec(frame); 397 398 if (ref) { 399 var m = reRef.exec(ref[1]); 400 if (m) { // If falsey, we did not get any file/line information 401 var file = m[1], lineno = m[2], charno = m[3] || 0; 402 if (file && this.isSameDomain(file) && lineno) { 403 var functionName = this.guessAnonymousFunction(file, lineno, charno); 404 stack[i] = frame.replace('{anonymous}', functionName); 405 } 406 } 407 } 408 } 409 return stack; 410 }, 411
412 guessAnonymousFunction: function(url, lineNo, charNo) { 413 var ret; 414 try { 415 ret = this.findFunctionName(this.getSource(url), lineNo); 416 } catch (e) { 417 ret = 'getSource failed with url: ' + url + ', exception: ' + e.toString(); 418 } 419 return ret; 420 }, 421 422 findFunctionName: function(source, lineNo) { 423 // FIXME findFunctionName fails for compressed source 424 // (more than one function on the same line) 425 // function {name}({args}) m[1]=name m[2]=args 426 var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/; 427 // {name} = function ({args}) TODO args capture 428 // /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function(?:[^(]*)/ 429 var reFunctionExpression = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/; 430 // {name} = eval() 431 var reFunctionEvaluation = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/; 432 // Walk backwards in the source lines until we find 433 // the line which matches one of the patterns above 434 var code = "", line, maxLines = Math.min(lineNo, 20), m, commentPos; 435 for (var i = 0; i < maxLines; ++i) { 436 // lineNo is 1-based, source[] is 0-based 437 line = source[lineNo - i - 1]; 438 commentPos = line.indexOf('//'); 439 if (commentPos >= 0) { 440 line = line.substr(0, commentPos); 441 } 442 // TODO check other types of comments? Commented code may lead to false positive 443 if (line) { 444 code = line + code; 445 m = reFunctionExpression.exec(code); 446 if (m && m[1]) { 447 return m[1]; 448 } 449 m = reFunctionDeclaration.exec(code); 450 if (m && m[1]) { 451 //return m[1] + "(" + (m[2] || "") + ")"; 452 return m[1]; 453 } 454 m = reFunctionEvaluation.exec(code); 455 if (m && m[1]) { 456 return m[1]; 457 } 458 } 459 } 460 return '(?)'; 461 } 462 }; 463 464 return printStackTrace; 465}));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.