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,76 +47,112 @@ | ||
| 47 | // OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 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 | * @cfg {Error} e The error to create a stacktrace from (optional) | 52 | * @cfg {Error} e The error to create a stacktrace from (optional) |
| 53 | * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions | 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 | function printStackTrace(options) { | 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 | printStackTrace.implementation.prototype = { | 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 | if (mode === 'other') { | 80 | if (mode === 'other') { |
| 80 | return this.other(arguments.callee); | 81 | return this.other(arguments.callee); |
| 81 | } else { | 82 | } else { |
| 82 | return this[mode](ex); | 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 | mode: function(e) { | 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 | } else if (e.stack) { | 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 | * Given a context, function name, and callback function, overwrite it so that it calls | 139 | * Given a context, function name, and callback function, overwrite it so that it calls |
| 104 | * printStackTrace() first with a callback and then runs the rest of the body. | 140 | * printStackTrace() first with a callback and then runs the rest of the body. |
| 105 | - * | 141 | + * |
| 106 | * @param {Object} context of execution (e.g. window) | 142 | * @param {Object} context of execution (e.g. window) |
| 107 | * @param {String} functionName to instrument | 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 | instrumentFunction: function(context, functionName, callback) { | 146 | instrumentFunction: function(context, functionName, callback) { |
| 111 | context = context || window; | 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 | * Given a context and function name of a function that has been | 157 | * Given a context and function name of a function that has been |
| 122 | * instrumented, revert the function to it's original (non-instrumented) | 158 | * instrumented, revert the function to it's original (non-instrumented) |
| @@ -128,134 +164,207 @@ printStackTrace.implementation.prototype = { | @@ -128,134 +164,207 @@ printStackTrace.implementation.prototype = { | ||
| 128 | deinstrumentFunction: function(context, functionName) { | 164 | deinstrumentFunction: function(context, functionName) { |
| 129 | if (context[functionName].constructor === Function && | 165 | if (context[functionName].constructor === Function && |
| 130 | context[functionName]._instrumented && | 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 | * Given an Error object, return a formatted Array based on Chrome's stack string. | 173 | * Given an Error object, return a formatted Array based on Chrome's stack string. |
| 138 | - * | 174 | + * |
| 139 | * @param e - Error object to inspect | 175 | * @param e - Error object to inspect |
| 140 | * @return Array<String> of function calls, files and line numbers | 176 | * @return Array<String> of function calls, files and line numbers |
| 141 | */ | 177 | */ |
| 142 | chrome: function(e) { | 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 | * Given an Error object, return a formatted Array based on Firefox's stack string. | 215 | * Given an Error object, return a formatted Array based on Firefox's stack string. |
| 148 | - * | 216 | + * |
| 149 | * @param e - Error object to inspect | 217 | * @param e - Error object to inspect |
| 150 | * @return Array<String> of function calls, files and line numbers | 218 | * @return Array<String> of function calls, files and line numbers |
| 151 | */ | 219 | */ |
| 152 | firefox: function(e) { | 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 | * Given an Error object, return a formatted Array based on Opera 10's stacktrace string. | 260 | * Given an Error object, return a formatted Array based on Opera 10's stacktrace string. |
| 158 | - * | 261 | + * |
| 159 | * @param e - Error object to inspect | 262 | * @param e - Error object to inspect |
| 160 | * @return Array<String> of function calls, files and line numbers | 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 | other: function(curr) { | 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 | fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON; | 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 | stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')'; | 305 | stack[stack.length] = fn + '(' + this.stringifyArguments(args) + ')'; |
| 205 | curr = curr.caller; | 306 | curr = curr.caller; |
| 206 | } | 307 | } |
| 207 | return stack; | 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 | stringifyArguments: function(args) { | 317 | stringifyArguments: function(args) { |
| 318 | + var result = []; | ||
| 319 | + var slice = Array.prototype.slice; | ||
| 217 | for (var i = 0; i < args.length; ++i) { | 320 | for (var i = 0; i < args.length; ++i) { |
| 218 | var arg = args[i]; | 321 | var arg = args[i]; |
| 219 | if (arg === undefined) { | 322 | if (arg === undefined) { |
| 220 | - args[i] = 'undefined'; | 323 | + result[i] = 'undefined'; |
| 221 | } else if (arg === null) { | 324 | } else if (arg === null) { |
| 222 | - args[i] = 'null'; | 325 | + result[i] = 'null'; |
| 223 | } else if (arg.constructor) { | 326 | } else if (arg.constructor) { |
| 224 | if (arg.constructor === Array) { | 327 | if (arg.constructor === Array) { |
| 225 | if (arg.length < 3) { | 328 | if (arg.length < 3) { |
| 226 | - args[i] = '[' + this.stringifyArguments(arg) + ']'; | 329 | + result[i] = '[' + this.stringifyArguments(arg) + ']'; |
| 227 | } else { | 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 | } else if (arg.constructor === Object) { | 333 | } else if (arg.constructor === Object) { |
| 231 | - args[i] = '#object'; | 334 | + result[i] = '#object'; |
| 232 | } else if (arg.constructor === Function) { | 335 | } else if (arg.constructor === Function) { |
| 233 | - args[i] = '#function'; | 336 | + result[i] = '#function'; |
| 234 | } else if (arg.constructor === String) { | 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 | sourceCache: {}, | 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 | ajax: function(url) { | 352 | ajax: function(url) { |
| 248 | var req = this.createXMLHTTPObject(); | 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 | * Try XHR methods in order and store XHR factory. | 369 | * Try XHR methods in order and store XHR factory. |
| 261 | * | 370 | * |
| @@ -279,7 +388,8 @@ printStackTrace.implementation.prototype = { | @@ -279,7 +388,8 @@ printStackTrace.implementation.prototype = { | ||
| 279 | // Use memoization to cache the factory | 388 | // Use memoization to cache the factory |
| 280 | this.createXMLHTTPObject = XMLHttpFactories[i]; | 389 | this.createXMLHTTPObject = XMLHttpFactories[i]; |
| 281 | return xmlhttp; | 390 | return xmlhttp; |
| 282 | - } catch (e) {} | 391 | + } catch (e) { |
| 392 | + } | ||
| 283 | } | 393 | } |
| 284 | }, | 394 | }, |
| 285 | 395 | ||
| @@ -288,12 +398,12 @@ printStackTrace.implementation.prototype = { | @@ -288,12 +398,12 @@ printStackTrace.implementation.prototype = { | ||
| 288 | * via Ajax). | 398 | * via Ajax). |
| 289 | * | 399 | * |
| 290 | * @param url <String> source url | 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 | isSameDomain: function(url) { | 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 | * Get source code from given URL if in the same domain. | 408 | * Get source code from given URL if in the same domain. |
| 299 | * | 409 | * |
| @@ -301,52 +411,78 @@ printStackTrace.implementation.prototype = { | @@ -301,52 +411,78 @@ printStackTrace.implementation.prototype = { | ||
| 301 | * @return <Array> Array of source code lines | 411 | * @return <Array> Array of source code lines |
| 302 | */ | 412 | */ |
| 303 | getSource: function(url) { | 413 | getSource: function(url) { |
| 414 | + // TODO reuse source from script tags? | ||
| 304 | if (!(url in this.sourceCache)) { | 415 | if (!(url in this.sourceCache)) { |
| 305 | this.sourceCache[url] = this.ajax(url).split('\n'); | 416 | this.sourceCache[url] = this.ajax(url).split('\n'); |
| 306 | } | 417 | } |
| 307 | return this.sourceCache[url]; | 418 | return this.sourceCache[url]; |
| 308 | }, | 419 | }, |
| 309 | - | ||
| 310 | - guessFunctions: function(stack) { | 420 | + |
| 421 | + guessAnonymousFunctions: function(stack) { | ||
| 311 | for (var i = 0; i < stack.length; ++i) { | 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 | return stack; | 438 | return stack; |
| 323 | }, | 439 | }, |
| 324 | - | ||
| 325 | - guessFunctionName: function(url, lineNo) { | 440 | + |
| 441 | + guessAnonymousFunction: function(url, lineNo, charNo) { | ||
| 442 | + var ret; | ||
| 326 | try { | 443 | try { |
| 327 | - return this.guessFunctionNameFromLines(lineNo, this.getSource(url)); | 444 | + ret = this.findFunctionName(this.getSource(url), lineNo); |
| 328 | } catch (e) { | 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 | for (var i = 0; i < maxLines; ++i) { | 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 | if (m && m[1]) { | 484 | if (m && m[1]) { |
| 344 | return m[1]; | 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,6 +515,11 @@ printStackTrace.implementation.prototype = { | ||
| 379 | '<project-root>{project_root}</project-root>' + | 515 | '<project-root>{project_root}</project-root>' + |
| 380 | '<environment-name>{environment}</environment-name>' + | 516 | '<environment-name>{environment}</environment-name>' + |
| 381 | '</server-environment>' + | 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 | '</notice>', | 523 | '</notice>', |
| 383 | REQUEST_VARIABLE_GROUP_XML = '<{group_name}>{inner_content}</{group_name}>', | 524 | REQUEST_VARIABLE_GROUP_XML = '<{group_name}>{inner_content}</{group_name}>', |
| 384 | REQUEST_VARIABLE_XML = '<var key="{key}">{value}</var>', | 525 | REQUEST_VARIABLE_XML = '<var key="{key}">{value}</var>', |
| @@ -411,9 +552,9 @@ printStackTrace.implementation.prototype = { | @@ -411,9 +552,9 @@ printStackTrace.implementation.prototype = { | ||
| 411 | "rootDirectory": "{project_root}", | 552 | "rootDirectory": "{project_root}", |
| 412 | "action": "{request_action}", | 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 | "environment": {}, | 559 | "environment": {}, |
| 419 | //"session": "", | 560 | //"session": "", |
| @@ -683,6 +824,15 @@ printStackTrace.implementation.prototype = { | @@ -683,6 +824,15 @@ printStackTrace.implementation.prototype = { | ||
| 683 | variable: 'outputFormat', | 824 | variable: 'outputFormat', |
| 684 | namespace: 'options' | 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 | methodName: 'setTrackJQ', | 836 | methodName: 'setTrackJQ', |
| 687 | variable: 'trackJQ', | 837 | variable: 'trackJQ', |
| 688 | namespace: 'options', | 838 | namespace: 'options', |