Commit 2776db24f43723f3e4ac64732fc29938bce93559
Exists in
master
and in
1 other branch
Merge branch 'master' of github.com:errbit/errbit
Showing
1 changed file
with
288 additions
and
138 deletions
Show diff stats
public/javascripts/notifier.js
| ... | ... | @@ -47,76 +47,112 @@ |
| 47 | 47 | // OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 48 | 48 | |
| 49 | 49 | /** |
| 50 | - * Main function giving a function stack trace with a forced or passed in Error | |
| 50 | + * Main function giving a function stack trace with a forced or passed in Error | |
| 51 | 51 | * |
| 52 | 52 | * @cfg {Error} e The error to create a stacktrace from (optional) |
| 53 | 53 | * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions |
| 54 | - * @return {Array} of Strings with functions, lines, files, and arguments where possible | |
| 54 | + * @return {Array} of Strings with functions, lines, files, and arguments where possible | |
| 55 | 55 | */ |
| 56 | 56 | function printStackTrace(options) { |
| 57 | - var ex = (options && options.e) ? options.e : null; | |
| 58 | - var guess = options ? !!options.guess : true; | |
| 59 | - | |
| 60 | - var p = new printStackTrace.implementation(); | |
| 61 | - var result = p.run(ex); | |
| 62 | - return (guess) ? p.guessFunctions(result) : result; | |
| 57 | + options = options || {guess: true}; | |
| 58 | + var ex = options.e || null, guess = !!options.guess; | |
| 59 | + var p = new printStackTrace.implementation(), result = p.run(ex); | |
| 60 | + return (guess) ? p.guessAnonymousFunctions(result) : result; | |
| 61 | +} | |
| 62 | + | |
| 63 | +if (typeof module !== "undefined" && module.exports) { | |
| 64 | + module.exports = printStackTrace; | |
| 63 | 65 | } |
| 64 | 66 | |
| 65 | -printStackTrace.implementation = function() {}; | |
| 67 | +printStackTrace.implementation = function() { | |
| 68 | +}; | |
| 66 | 69 | |
| 67 | 70 | printStackTrace.implementation.prototype = { |
| 68 | - run: function(ex) { | |
| 69 | - ex = ex || | |
| 70 | - (function() { | |
| 71 | - try { | |
| 72 | - var _err = __undef__ << 1; | |
| 73 | - } catch (e) { | |
| 74 | - return e; | |
| 75 | - } | |
| 76 | - })(); | |
| 77 | - // Use either the stored mode, or resolve it | |
| 78 | - var mode = this._mode || this.mode(ex); | |
| 71 | + /** | |
| 72 | + * @param {Error} ex The error to create a stacktrace from (optional) | |
| 73 | + * @param {String} mode Forced mode (optional, mostly for unit tests) | |
| 74 | + */ | |
| 75 | + run: function(ex, mode) { | |
| 76 | + ex = ex || this.createException(); | |
| 77 | + // examine exception properties w/o debugger | |
| 78 | + //for (var prop in ex) {alert("Ex['" + prop + "']=" + ex[prop]);} | |
| 79 | + mode = mode || this.mode(ex); | |
| 79 | 80 | if (mode === 'other') { |
| 80 | 81 | return this.other(arguments.callee); |
| 81 | 82 | } else { |
| 82 | 83 | return this[mode](ex); |
| 83 | 84 | } |
| 84 | 85 | }, |
| 85 | - | |
| 86 | + | |
| 87 | + createException: function() { | |
| 88 | + try { | |
| 89 | + this.undef(); | |
| 90 | + } catch (e) { | |
| 91 | + return e; | |
| 92 | + } | |
| 93 | + }, | |
| 94 | + | |
| 86 | 95 | /** |
| 87 | - * @return {String} mode of operation for the environment in question. | |
| 96 | + * Mode could differ for different exception, e.g. | |
| 97 | + * exceptions in Chrome may or may not have arguments or stack. | |
| 98 | + * | |
| 99 | + * @return {String} mode of operation for the exception | |
| 88 | 100 | */ |
| 89 | 101 | mode: function(e) { |
| 90 | - if (e['arguments']) { | |
| 91 | - return (this._mode = 'chrome'); | |
| 92 | - } else if (window.opera && e.stacktrace) { | |
| 93 | - return (this._mode = 'opera10'); | |
| 102 | + if (e['arguments'] && e.stack) { | |
| 103 | + return 'chrome'; | |
| 104 | + } else if (e.stack && e.sourceURL) { | |
| 105 | + return 'safari'; | |
| 106 | + } else if (e.stack && e.number) { | |
| 107 | + return 'ie'; | |
| 108 | + } else if (typeof e.message === 'string' && typeof window !== 'undefined' && window.opera) { | |
| 109 | + // e.message.indexOf("Backtrace:") > -1 -> opera | |
| 110 | + // !e.stacktrace -> opera | |
| 111 | + if (!e.stacktrace) { | |
| 112 | + return 'opera9'; // use e.message | |
| 113 | + } | |
| 114 | + // 'opera#sourceloc' in e -> opera9, opera10a | |
| 115 | + if (e.message.indexOf('\n') > -1 && e.message.split('\n').length > e.stacktrace.split('\n').length) { | |
| 116 | + return 'opera9'; // use e.message | |
| 117 | + } | |
| 118 | + // e.stacktrace && !e.stack -> opera10a | |
| 119 | + if (!e.stack) { | |
| 120 | + return 'opera10a'; // use e.stacktrace | |
| 121 | + } | |
| 122 | + // e.stacktrace && e.stack -> opera10b | |
| 123 | + if (e.stacktrace.indexOf("called from line") < 0) { | |
| 124 | + return 'opera10b'; // use e.stacktrace, format differs from 'opera10a' | |
| 125 | + } | |
| 126 | + // e.stacktrace && e.stack -> opera11 | |
| 127 | + return 'opera11'; // use e.stacktrace, format differs from 'opera10a', 'opera10b' | |
| 128 | + } else if (e.stack && !e.fileName) { | |
| 129 | + // Chrome 27 does not have e.arguments as earlier versions, | |
| 130 | + // but still does not have e.fileName as Firefox | |
| 131 | + return 'chrome'; | |
| 94 | 132 | } else if (e.stack) { |
| 95 | - return (this._mode = 'firefox'); | |
| 96 | - } else if (window.opera && !('stacktrace' in e)) { //Opera 9- | |
| 97 | - return (this._mode = 'opera'); | |
| 133 | + return 'firefox'; | |
| 98 | 134 | } |
| 99 | - return (this._mode = 'other'); | |
| 135 | + return 'other'; | |
| 100 | 136 | }, |
| 101 | 137 | |
| 102 | 138 | /** |
| 103 | 139 | * Given a context, function name, and callback function, overwrite it so that it calls |
| 104 | 140 | * printStackTrace() first with a callback and then runs the rest of the body. |
| 105 | - * | |
| 141 | + * | |
| 106 | 142 | * @param {Object} context of execution (e.g. window) |
| 107 | 143 | * @param {String} functionName to instrument |
| 108 | - * @param {Function} function to call with a stack trace on invocation | |
| 144 | + * @param {Function} callback function to call with a stack trace on invocation | |
| 109 | 145 | */ |
| 110 | 146 | instrumentFunction: function(context, functionName, callback) { |
| 111 | 147 | context = context || window; |
| 112 | - context['_old' + functionName] = context[functionName]; | |
| 113 | - context[functionName] = function() { | |
| 114 | - callback.call(this, printStackTrace()); | |
| 115 | - return context['_old' + functionName].apply(this, arguments); | |
| 148 | + var original = context[functionName]; | |
| 149 | + context[functionName] = function instrumented() { | |
| 150 | + callback.call(this, printStackTrace().slice(4)); | |
| 151 | + return context[functionName]._instrumented.apply(this, arguments); | |
| 116 | 152 | }; |
| 117 | - context[functionName]._instrumented = true; | |
| 153 | + context[functionName]._instrumented = original; | |
| 118 | 154 | }, |
| 119 | - | |
| 155 | + | |
| 120 | 156 | /** |
| 121 | 157 | * Given a context and function name of a function that has been |
| 122 | 158 | * instrumented, revert the function to it's original (non-instrumented) |
| ... | ... | @@ -128,134 +164,207 @@ printStackTrace.implementation.prototype = { |
| 128 | 164 | deinstrumentFunction: function(context, functionName) { |
| 129 | 165 | if (context[functionName].constructor === Function && |
| 130 | 166 | context[functionName]._instrumented && |
| 131 | - context['_old' + functionName].constructor === Function) { | |
| 132 | - context[functionName] = context['_old' + functionName]; | |
| 167 | + context[functionName]._instrumented.constructor === Function) { | |
| 168 | + context[functionName] = context[functionName]._instrumented; | |
| 133 | 169 | } |
| 134 | 170 | }, |
| 135 | - | |
| 171 | + | |
| 136 | 172 | /** |
| 137 | 173 | * Given an Error object, return a formatted Array based on Chrome's stack string. |
| 138 | - * | |
| 174 | + * | |
| 139 | 175 | * @param e - Error object to inspect |
| 140 | 176 | * @return Array<String> of function calls, files and line numbers |
| 141 | 177 | */ |
| 142 | 178 | chrome: function(e) { |
| 143 | - return e.stack.replace(/^[^\(]+?[\n$]/gm, '').replace(/^\s+at\s+/gm, '').replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@').split('\n'); | |
| 179 | + var stack = (e.stack + '\n').replace(/^\S[^\(]+?[\n$]/gm, ''). | |
| 180 | + replace(/^\s+(at eval )?at\s+/gm, ''). | |
| 181 | + replace(/^([^\(]+?)([\n$])/gm, '{anonymous}()@$1$2'). | |
| 182 | + replace(/^Object.<anonymous>\s*\(([^\)]+)\)/gm, '{anonymous}()@$1').split('\n'); | |
| 183 | + stack.pop(); | |
| 184 | + return stack; | |
| 185 | + }, | |
| 186 | + | |
| 187 | + /** | |
| 188 | + * Given an Error object, return a formatted Array based on Safari's stack string. | |
| 189 | + * | |
| 190 | + * @param e - Error object to inspect | |
| 191 | + * @return Array<String> of function calls, files and line numbers | |
| 192 | + */ | |
| 193 | + safari: function(e) { | |
| 194 | + return e.stack.replace(/\[native code\]\n/m, '') | |
| 195 | + .replace(/^(?=\w+Error\:).*$\n/m, '') | |
| 196 | + .replace(/^@/gm, '{anonymous}()@') | |
| 197 | + .split('\n'); | |
| 198 | + }, | |
| 199 | + | |
| 200 | + /** | |
| 201 | + * Given an Error object, return a formatted Array based on IE's stack string. | |
| 202 | + * | |
| 203 | + * @param e - Error object to inspect | |
| 204 | + * @return Array<String> of function calls, files and line numbers | |
| 205 | + */ | |
| 206 | + ie: function(e) { | |
| 207 | + var lineRE = /^.*at (\w+) \(([^\)]+)\)$/gm; | |
| 208 | + return e.stack.replace(/at Anonymous function /gm, '{anonymous}()@') | |
| 209 | + .replace(/^(?=\w+Error\:).*$\n/m, '') | |
| 210 | + .replace(lineRE, '$1@$2') | |
| 211 | + .split('\n'); | |
| 144 | 212 | }, |
| 145 | 213 | |
| 146 | 214 | /** |
| 147 | 215 | * Given an Error object, return a formatted Array based on Firefox's stack string. |
| 148 | - * | |
| 216 | + * | |
| 149 | 217 | * @param e - Error object to inspect |
| 150 | 218 | * @return Array<String> of function calls, files and line numbers |
| 151 | 219 | */ |
| 152 | 220 | firefox: function(e) { |
| 153 | - return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^\(/gm, '{anonymous}(').split('\n'); | |
| 221 | + return e.stack.replace(/(?:\n@:0)?\s+$/m, '').replace(/^[\(@]/gm, '{anonymous}()@').split('\n'); | |
| 222 | + }, | |
| 223 | + | |
| 224 | + opera11: function(e) { | |
| 225 | + var ANON = '{anonymous}', lineRE = /^.*line (\d+), column (\d+)(?: in (.+))? in (\S+):$/; | |
| 226 | + var lines = e.stacktrace.split('\n'), result = []; | |
| 227 | + | |
| 228 | + for (var i = 0, len = lines.length; i < len; i += 2) { | |
| 229 | + var match = lineRE.exec(lines[i]); | |
| 230 | + if (match) { | |
| 231 | + var location = match[4] + ':' + match[1] + ':' + match[2]; | |
| 232 | + var fnName = match[3] || "global code"; | |
| 233 | + fnName = fnName.replace(/<anonymous function: (\S+)>/, "$1").replace(/<anonymous function>/, ANON); | |
| 234 | + result.push(fnName + '@' + location + ' -- ' + lines[i + 1].replace(/^\s+/, '')); | |
| 235 | + } | |
| 236 | + } | |
| 237 | + | |
| 238 | + return result; | |
| 239 | + }, | |
| 240 | + | |
| 241 | + opera10b: function(e) { | |
| 242 | + // "<anonymous function: run>([arguments not available])@file://localhost/G:/js/stacktrace.js:27\n" + | |
| 243 | + // "printStackTrace([arguments not available])@file://localhost/G:/js/stacktrace.js:18\n" + | |
| 244 | + // "@file://localhost/G:/js/test/functional/testcase1.html:15" | |
| 245 | + var lineRE = /^(.*)@(.+):(\d+)$/; | |
| 246 | + var lines = e.stacktrace.split('\n'), result = []; | |
| 247 | + | |
| 248 | + for (var i = 0, len = lines.length; i < len; i++) { | |
| 249 | + var match = lineRE.exec(lines[i]); | |
| 250 | + if (match) { | |
| 251 | + var fnName = match[1]? (match[1] + '()') : "global code"; | |
| 252 | + result.push(fnName + '@' + match[2] + ':' + match[3]); | |
| 253 | + } | |
| 254 | + } | |
| 255 | + | |
| 256 | + return result; | |
| 154 | 257 | }, |
| 155 | 258 | |
| 156 | 259 | /** |
| 157 | 260 | * Given an Error object, return a formatted Array based on Opera 10's stacktrace string. |
| 158 | - * | |
| 261 | + * | |
| 159 | 262 | * @param e - Error object to inspect |
| 160 | 263 | * @return Array<String> of function calls, files and line numbers |
| 161 | 264 | */ |
| 162 | - opera10: function(e) { | |
| 163 | - var stack = e.stacktrace; | |
| 164 | - var lines = stack.split('\n'), ANON = '{anonymous}', | |
| 165 | - lineRE = /.*line (\d+), column (\d+) in ((<anonymous function\:?\s*(\S+))|([^\(]+)\([^\)]*\))(?: in )?(.*)\s*$/i, i, j, len; | |
| 166 | - for (i = 2, j = 0, len = lines.length; i < len - 2; i++) { | |
| 167 | - if (lineRE.test(lines[i])) { | |
| 168 | - var location = RegExp.$6 + ':' + RegExp.$1 + ':' + RegExp.$2; | |
| 169 | - var fnName = RegExp.$3; | |
| 170 | - fnName = fnName.replace(/<anonymous function\:?\s?(\S+)?>/g, ANON); | |
| 171 | - lines[j++] = fnName + '@' + location; | |
| 265 | + opera10a: function(e) { | |
| 266 | + // " Line 27 of linked script file://localhost/G:/js/stacktrace.js\n" | |
| 267 | + // " Line 11 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html: In function foo\n" | |
| 268 | + var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)(?:: In function (\S+))?$/i; | |
| 269 | + var lines = e.stacktrace.split('\n'), result = []; | |
| 270 | + | |
| 271 | + for (var i = 0, len = lines.length; i < len; i += 2) { | |
| 272 | + var match = lineRE.exec(lines[i]); | |
| 273 | + if (match) { | |
| 274 | + var fnName = match[3] || ANON; | |
| 275 | + result.push(fnName + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, '')); | |
| 172 | 276 | } |
| 173 | 277 | } |
| 174 | - | |
| 175 | - lines.splice(j, lines.length - j); | |
| 176 | - return lines; | |
| 278 | + | |
| 279 | + return result; | |
| 177 | 280 | }, |
| 178 | - | |
| 179 | - // Opera 7.x-9.x only! | |
| 180 | - opera: function(e) { | |
| 181 | - var lines = e.message.split('\n'), ANON = '{anonymous}', | |
| 182 | - lineRE = /Line\s+(\d+).*script\s+(http\S+)(?:.*in\s+function\s+(\S+))?/i, | |
| 183 | - i, j, len; | |
| 184 | - | |
| 185 | - for (i = 4, j = 0, len = lines.length; i < len; i += 2) { | |
| 186 | - //TODO: RegExp.exec() would probably be cleaner here | |
| 187 | - if (lineRE.test(lines[i])) { | |
| 188 | - lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) + ' -- ' + lines[i + 1].replace(/^\s+/, ''); | |
| 281 | + | |
| 282 | + // Opera 7.x-9.2x only! | |
| 283 | + opera9: function(e) { | |
| 284 | + // " Line 43 of linked script file://localhost/G:/js/stacktrace.js\n" | |
| 285 | + // " Line 7 of inline#1 script in file://localhost/G:/js/test/functional/testcase1.html\n" | |
| 286 | + var ANON = '{anonymous}', lineRE = /Line (\d+).*script (?:in )?(\S+)/i; | |
| 287 | + var lines = e.message.split('\n'), result = []; | |
| 288 | + | |
| 289 | + for (var i = 2, len = lines.length; i < len; i += 2) { | |
| 290 | + var match = lineRE.exec(lines[i]); | |
| 291 | + if (match) { | |
| 292 | + result.push(ANON + '()@' + match[2] + ':' + match[1] + ' -- ' + lines[i + 1].replace(/^\s+/, '')); | |
| 189 | 293 | } |
| 190 | 294 | } |
| 191 | - | |
| 192 | - lines.splice(j, lines.length - j); | |
| 193 | - return lines; | |
| 295 | + | |
| 296 | + return result; | |
| 194 | 297 | }, |
| 195 | - | |
| 196 | - // Safari, IE, and others | |
| 298 | + | |
| 299 | + // Safari 5-, IE 9-, and others | |
| 197 | 300 | other: function(curr) { |
| 198 | - var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, | |
| 199 | - stack = [], fn, args, maxStackSize = 10; | |
| 200 | - | |
| 201 | - while (curr && stack.length < maxStackSize) { | |
| 301 | + var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], fn, args, maxStackSize = 10; | |
| 302 | + while (curr && curr['arguments'] && stack.length < maxStackSize) { | |
| 202 | 303 | fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON; |
| 203 | - args = Array.prototype.slice.call(curr['arguments']); | |
| 304 | + args = Array.prototype.slice.call(curr['arguments'] || []); | |
| 204 | 305 | stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')'; |
| 205 | 306 | curr = curr.caller; |
| 206 | 307 | } |
| 207 | 308 | return stack; |
| 208 | 309 | }, |
| 209 | - | |
| 310 | + | |
| 210 | 311 | /** |
| 211 | - * Given arguments array as a String, subsituting type names for non-string types. | |
| 312 | + * Given arguments array as a String, substituting type names for non-string types. | |
| 212 | 313 | * |
| 213 | - * @param {Arguments} object | |
| 214 | - * @return {Array} of Strings with stringified arguments | |
| 314 | + * @param {Arguments,Array} args | |
| 315 | + * @return {String} stringified arguments | |
| 215 | 316 | */ |
| 216 | 317 | stringifyArguments: function(args) { |
| 318 | + var result = []; | |
| 319 | + var slice = Array.prototype.slice; | |
| 217 | 320 | for (var i = 0; i < args.length; ++i) { |
| 218 | 321 | var arg = args[i]; |
| 219 | 322 | if (arg === undefined) { |
| 220 | - args[i] = 'undefined'; | |
| 323 | + result[i] = 'undefined'; | |
| 221 | 324 | } else if (arg === null) { |
| 222 | - args[i] = 'null'; | |
| 325 | + result[i] = 'null'; | |
| 223 | 326 | } else if (arg.constructor) { |
| 224 | 327 | if (arg.constructor === Array) { |
| 225 | 328 | if (arg.length < 3) { |
| 226 | - args[i] = '[' + this.stringifyArguments(arg) + ']'; | |
| 329 | + result[i] = '[' + this.stringifyArguments(arg) + ']'; | |
| 227 | 330 | } else { |
| 228 | - args[i] = '[' + this.stringifyArguments(Array.prototype.slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(Array.prototype.slice.call(arg, -1)) + ']'; | |
| 331 | + result[i] = '[' + this.stringifyArguments(slice.call(arg, 0, 1)) + '...' + this.stringifyArguments(slice.call(arg, -1)) + ']'; | |
| 229 | 332 | } |
| 230 | 333 | } else if (arg.constructor === Object) { |
| 231 | - args[i] = '#object'; | |
| 334 | + result[i] = '#object'; | |
| 232 | 335 | } else if (arg.constructor === Function) { |
| 233 | - args[i] = '#function'; | |
| 336 | + result[i] = '#function'; | |
| 234 | 337 | } else if (arg.constructor === String) { |
| 235 | - args[i] = '"' + arg + '"'; | |
| 338 | + result[i] = '"' + arg + '"'; | |
| 339 | + } else if (arg.constructor === Number) { | |
| 340 | + result[i] = arg; | |
| 236 | 341 | } |
| 237 | 342 | } |
| 238 | 343 | } |
| 239 | - return args.join(','); | |
| 344 | + return result.join(','); | |
| 240 | 345 | }, |
| 241 | - | |
| 346 | + | |
| 242 | 347 | sourceCache: {}, |
| 243 | - | |
| 348 | + | |
| 244 | 349 | /** |
| 245 | - * @return the text from a given URL. | |
| 350 | + * @return the text from a given URL | |
| 246 | 351 | */ |
| 247 | 352 | ajax: function(url) { |
| 248 | 353 | var req = this.createXMLHTTPObject(); |
| 249 | - if (!req) { | |
| 250 | - return; | |
| 354 | + if (req) { | |
| 355 | + try { | |
| 356 | + req.open('GET', url, false); | |
| 357 | + //req.overrideMimeType('text/plain'); | |
| 358 | + //req.overrideMimeType('text/javascript'); | |
| 359 | + req.send(null); | |
| 360 | + //return req.status == 200 ? req.responseText : ''; | |
| 361 | + return req.responseText; | |
| 362 | + } catch (e) { | |
| 363 | + } | |
| 251 | 364 | } |
| 252 | - req.open('GET', url, false); | |
| 253 | - // REMOVED FOR JS TEST. | |
| 254 | - //req.setRequestHeader('User-Agent', 'XMLHTTP/1.0'); | |
| 255 | - req.send(''); | |
| 256 | - return req.responseText; | |
| 365 | + return ''; | |
| 257 | 366 | }, |
| 258 | - | |
| 367 | + | |
| 259 | 368 | /** |
| 260 | 369 | * Try XHR methods in order and store XHR factory. |
| 261 | 370 | * |
| ... | ... | @@ -279,7 +388,8 @@ printStackTrace.implementation.prototype = { |
| 279 | 388 | // Use memoization to cache the factory |
| 280 | 389 | this.createXMLHTTPObject = XMLHttpFactories[i]; |
| 281 | 390 | return xmlhttp; |
| 282 | - } catch (e) {} | |
| 391 | + } catch (e) { | |
| 392 | + } | |
| 283 | 393 | } |
| 284 | 394 | }, |
| 285 | 395 | |
| ... | ... | @@ -288,12 +398,12 @@ printStackTrace.implementation.prototype = { |
| 288 | 398 | * via Ajax). |
| 289 | 399 | * |
| 290 | 400 | * @param url <String> source url |
| 291 | - * @return False if we need a cross-domain request | |
| 401 | + * @return <Boolean> False if we need a cross-domain request | |
| 292 | 402 | */ |
| 293 | 403 | isSameDomain: function(url) { |
| 294 | - return url.indexOf(location.hostname) !== -1; | |
| 404 | + return typeof location !== "undefined" && url.indexOf(location.hostname) !== -1; // location may not be defined, e.g. when running from nodejs. | |
| 295 | 405 | }, |
| 296 | - | |
| 406 | + | |
| 297 | 407 | /** |
| 298 | 408 | * Get source code from given URL if in the same domain. |
| 299 | 409 | * |
| ... | ... | @@ -301,52 +411,78 @@ printStackTrace.implementation.prototype = { |
| 301 | 411 | * @return <Array> Array of source code lines |
| 302 | 412 | */ |
| 303 | 413 | getSource: function(url) { |
| 414 | + // TODO reuse source from script tags? | |
| 304 | 415 | if (!(url in this.sourceCache)) { |
| 305 | 416 | this.sourceCache[url] = this.ajax(url).split('\n'); |
| 306 | 417 | } |
| 307 | 418 | return this.sourceCache[url]; |
| 308 | 419 | }, |
| 309 | - | |
| 310 | - guessFunctions: function(stack) { | |
| 420 | + | |
| 421 | + guessAnonymousFunctions: function(stack) { | |
| 311 | 422 | for (var i = 0; i < stack.length; ++i) { |
| 312 | - var reStack = /\{anonymous\}\(.*\)@(\w+:\/\/([\-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/; | |
| 313 | - var frame = stack[i], m = reStack.exec(frame); | |
| 314 | - if (m) { | |
| 315 | - var file = m[1], lineno = m[4]; //m[7] is character position in Chrome | |
| 316 | - if (file && this.isSameDomain(file) && lineno) { | |
| 317 | - var functionName = this.guessFunctionName(file, lineno); | |
| 318 | - stack[i] = frame.replace('{anonymous}', functionName); | |
| 423 | + var reStack = /\{anonymous\}\(.*\)@(.*)/, | |
| 424 | + reRef = /^(.*?)(?::(\d+))(?::(\d+))?(?: -- .+)?$/, | |
| 425 | + frame = stack[i], ref = reStack.exec(frame); | |
| 426 | + | |
| 427 | + if (ref) { | |
| 428 | + var m = reRef.exec(ref[1]); | |
| 429 | + if (m) { // If falsey, we did not get any file/line information | |
| 430 | + var file = m[1], lineno = m[2], charno = m[3] || 0; | |
| 431 | + if (file && this.isSameDomain(file) && lineno) { | |
| 432 | + var functionName = this.guessAnonymousFunction(file, lineno, charno); | |
| 433 | + stack[i] = frame.replace('{anonymous}', functionName); | |
| 434 | + } | |
| 319 | 435 | } |
| 320 | 436 | } |
| 321 | 437 | } |
| 322 | 438 | return stack; |
| 323 | 439 | }, |
| 324 | - | |
| 325 | - guessFunctionName: function(url, lineNo) { | |
| 440 | + | |
| 441 | + guessAnonymousFunction: function(url, lineNo, charNo) { | |
| 442 | + var ret; | |
| 326 | 443 | try { |
| 327 | - return this.guessFunctionNameFromLines(lineNo, this.getSource(url)); | |
| 444 | + ret = this.findFunctionName(this.getSource(url), lineNo); | |
| 328 | 445 | } catch (e) { |
| 329 | - return 'getSource failed with url: ' + url + ', exception: ' + e.toString(); | |
| 446 | + ret = 'getSource failed with url: ' + url + ', exception: ' + e.toString(); | |
| 330 | 447 | } |
| 448 | + return ret; | |
| 331 | 449 | }, |
| 332 | - | |
| 333 | - guessFunctionNameFromLines: function(lineNo, source) { | |
| 334 | - var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/; | |
| 335 | - var reGuessFunction = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/; | |
| 336 | - // Walk backwards from the first line in the function until we find the line which | |
| 337 | - // matches the pattern above, which is the function definition | |
| 338 | - var line = "", maxLines = 10; | |
| 450 | + | |
| 451 | + findFunctionName: function(source, lineNo) { | |
| 452 | + // FIXME findFunctionName fails for compressed source | |
| 453 | + // (more than one function on the same line) | |
| 454 | + // function {name}({args}) m[1]=name m[2]=args | |
| 455 | + var reFunctionDeclaration = /function\s+([^(]*?)\s*\(([^)]*)\)/; | |
| 456 | + // {name} = function ({args}) TODO args capture | |
| 457 | + // /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*function(?:[^(]*)/ | |
| 458 | + var reFunctionExpression = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*function\b/; | |
| 459 | + // {name} = eval() | |
| 460 | + var reFunctionEvaluation = /['"]?([$_A-Za-z][$_A-Za-z0-9]*)['"]?\s*[:=]\s*(?:eval|new Function)\b/; | |
| 461 | + // Walk backwards in the source lines until we find | |
| 462 | + // the line which matches one of the patterns above | |
| 463 | + var code = "", line, maxLines = Math.min(lineNo, 20), m, commentPos; | |
| 339 | 464 | for (var i = 0; i < maxLines; ++i) { |
| 340 | - line = source[lineNo - i] + line; | |
| 341 | - if (line !== undefined) { | |
| 342 | - var m = reGuessFunction.exec(line); | |
| 465 | + // lineNo is 1-based, source[] is 0-based | |
| 466 | + line = source[lineNo - i - 1]; | |
| 467 | + commentPos = line.indexOf('//'); | |
| 468 | + if (commentPos >= 0) { | |
| 469 | + line = line.substr(0, commentPos); | |
| 470 | + } | |
| 471 | + // TODO check other types of comments? Commented code may lead to false positive | |
| 472 | + if (line) { | |
| 473 | + code = line + code; | |
| 474 | + m = reFunctionExpression.exec(code); | |
| 475 | + if (m && m[1]) { | |
| 476 | + return m[1]; | |
| 477 | + } | |
| 478 | + m = reFunctionDeclaration.exec(code); | |
| 479 | + if (m && m[1]) { | |
| 480 | + //return m[1] + "(" + (m[2] || "") + ")"; | |
| 481 | + return m[1]; | |
| 482 | + } | |
| 483 | + m = reFunctionEvaluation.exec(code); | |
| 343 | 484 | if (m && m[1]) { |
| 344 | 485 | return m[1]; |
| 345 | - } else { | |
| 346 | - m = reFunctionArgNames.exec(line); | |
| 347 | - if (m && m[1]) { | |
| 348 | - return m[1]; | |
| 349 | - } | |
| 350 | 486 | } |
| 351 | 487 | } |
| 352 | 488 | } |
| ... | ... | @@ -379,6 +515,11 @@ printStackTrace.implementation.prototype = { |
| 379 | 515 | '<project-root>{project_root}</project-root>' + |
| 380 | 516 | '<environment-name>{environment}</environment-name>' + |
| 381 | 517 | '</server-environment>' + |
| 518 | + '<current-user>' + | |
| 519 | + '<id>{user_id}</id>' + | |
| 520 | + '<name>{user_name}</name>' + | |
| 521 | + '<email>{user_email}</email>' + | |
| 522 | + '</current-user>' + | |
| 382 | 523 | '</notice>', |
| 383 | 524 | REQUEST_VARIABLE_GROUP_XML = '<{group_name}>{inner_content}</{group_name}>', |
| 384 | 525 | REQUEST_VARIABLE_XML = '<var key="{key}">{value}</var>', |
| ... | ... | @@ -411,9 +552,9 @@ printStackTrace.implementation.prototype = { |
| 411 | 552 | "rootDirectory": "{project_root}", |
| 412 | 553 | "action": "{request_action}", |
| 413 | 554 | |
| 414 | - "userId": "{}", | |
| 415 | - "userName": "{}", | |
| 416 | - "userEmail": "{}", | |
| 555 | + "userId": "{user_id}", | |
| 556 | + "userName": "{user_name}", | |
| 557 | + "userEmail": "{user_email}", | |
| 417 | 558 | }, |
| 418 | 559 | "environment": {}, |
| 419 | 560 | //"session": "", |
| ... | ... | @@ -683,6 +824,15 @@ printStackTrace.implementation.prototype = { |
| 683 | 824 | variable: 'outputFormat', |
| 684 | 825 | namespace: 'options' |
| 685 | 826 | }, { |
| 827 | + methodName: 'setCurrentUser', | |
| 828 | + method: (function (value) { | |
| 829 | + for (var key in value) { | |
| 830 | + if (value.hasOwnProperty(key)) { | |
| 831 | + Config.xmlData['user_' + key] = value[key]; | |
| 832 | + } | |
| 833 | + } | |
| 834 | + }) | |
| 835 | + }, { | |
| 686 | 836 | methodName: 'setTrackJQ', |
| 687 | 837 | variable: 'trackJQ', |
| 688 | 838 | namespace: 'options', | ... | ... |