├── .gitignore ├── LICENSE.md ├── README.md ├── dist ├── opbeat.js └── opbeat.min.js ├── example ├── example.js └── index.html ├── gulpfile.js ├── libs └── tracekit.js ├── package.json └── src └── opbeat.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Matt Robenolt, Vanja Cosic and other contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Opbeat.js 2 | 3 | Experimental client for logging errors (exceptions) and stacktraces to [Opbeat](http://opbeat.com/) from within 4 | your client-side (browser) JavaScript applications. 5 | 6 | -- 7 | 8 | ### This project has now been deprecated! ⚠️ 9 | 10 | For access to client side JavaScript error logging, please contact [support@opbeat.com](mailto:support@opbeat.com). 11 | The original README is still available [here](https://github.com/vanjacosic/opbeat-js/blob/84246fd8aa19aa7b3ad757c29df215fae6fb832e/README.md). 12 | 13 | -- 14 | For Node.js applications, please see the [opbeat-node](https://github.com/opbeat/opbeat-node) client instead. 15 | 16 | -------------------------------------------------------------------------------- /dist/opbeat.js: -------------------------------------------------------------------------------- 1 | /* 2 | TraceKit - Cross brower stack traces - github.com/occ/TraceKit 3 | MIT license 4 | */ 5 | 6 | (function(window, undefined) { 7 | 8 | 9 | var TraceKit = {}; 10 | var _oldTraceKit = window.TraceKit; 11 | 12 | // global reference to slice 13 | var _slice = [].slice; 14 | var UNKNOWN_FUNCTION = '?'; 15 | 16 | 17 | /** 18 | * _has, a better form of hasOwnProperty 19 | * Example: _has(MainHostObject, property) === true/false 20 | * 21 | * @param {Object} host object to check property 22 | * @param {string} key to check 23 | */ 24 | function _has(object, key) { 25 | return Object.prototype.hasOwnProperty.call(object, key); 26 | } 27 | 28 | function _isUndefined(what) { 29 | return typeof what === 'undefined'; 30 | } 31 | 32 | /** 33 | * TraceKit.noConflict: Export TraceKit out to another variable 34 | * Example: var TK = TraceKit.noConflict() 35 | */ 36 | TraceKit.noConflict = function noConflict() { 37 | window.TraceKit = _oldTraceKit; 38 | return TraceKit; 39 | }; 40 | 41 | /** 42 | * TraceKit.wrap: Wrap any function in a TraceKit reporter 43 | * Example: func = TraceKit.wrap(func); 44 | * 45 | * @param {Function} func Function to be wrapped 46 | * @return {Function} The wrapped func 47 | */ 48 | TraceKit.wrap = function traceKitWrapper(func) { 49 | function wrapped() { 50 | try { 51 | return func.apply(this, arguments); 52 | } catch (e) { 53 | TraceKit.report(e); 54 | throw e; 55 | } 56 | } 57 | return wrapped; 58 | }; 59 | 60 | /** 61 | * TraceKit.report: cross-browser processing of unhandled exceptions 62 | * 63 | * Syntax: 64 | * TraceKit.report.subscribe(function(stackInfo) { ... }) 65 | * TraceKit.report.unsubscribe(function(stackInfo) { ... }) 66 | * TraceKit.report(exception) 67 | * try { ...code... } catch(ex) { TraceKit.report(ex); } 68 | * 69 | * Supports: 70 | * - Firefox: full stack trace with line numbers, plus column number 71 | * on top frame; column number is not guaranteed 72 | * - Opera: full stack trace with line and column numbers 73 | * - Chrome: full stack trace with line and column numbers 74 | * - Safari: line and column number for the top frame only; some frames 75 | * may be missing, and column number is not guaranteed 76 | * - IE: line and column number for the top frame only; some frames 77 | * may be missing, and column number is not guaranteed 78 | * 79 | * In theory, TraceKit should work on all of the following versions: 80 | * - IE5.5+ (only 8.0 tested) 81 | * - Firefox 0.9+ (only 3.5+ tested) 82 | * - Opera 7+ (only 10.50 tested; versions 9 and earlier may require 83 | * Exceptions Have Stacktrace to be enabled in opera:config) 84 | * - Safari 3+ (only 4+ tested) 85 | * - Chrome 1+ (only 5+ tested) 86 | * - Konqueror 3.5+ (untested) 87 | * 88 | * Requires TraceKit.computeStackTrace. 89 | * 90 | * Tries to catch all unhandled exceptions and report them to the 91 | * subscribed handlers. Please note that TraceKit.report will rethrow the 92 | * exception. This is REQUIRED in order to get a useful stack trace in IE. 93 | * If the exception does not reach the top of the browser, you will only 94 | * get a stack trace from the point where TraceKit.report was called. 95 | * 96 | * Handlers receive a stackInfo object as described in the 97 | * TraceKit.computeStackTrace docs. 98 | */ 99 | TraceKit.report = (function reportModuleWrapper() { 100 | var handlers = [], 101 | lastException = null, 102 | lastExceptionStack = null; 103 | 104 | /** 105 | * Add a crash handler. 106 | * @param {Function} handler 107 | */ 108 | function subscribe(handler) { 109 | installGlobalHandler(); 110 | handlers.push(handler); 111 | } 112 | 113 | /** 114 | * Remove a crash handler. 115 | * @param {Function} handler 116 | */ 117 | function unsubscribe(handler) { 118 | for (var i = handlers.length - 1; i >= 0; --i) { 119 | if (handlers[i] === handler) { 120 | handlers.splice(i, 1); 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Dispatch stack information to all handlers. 127 | * @param {Object.} stack 128 | */ 129 | function notifyHandlers(stack, windowError) { 130 | var exception = null; 131 | if (windowError && !TraceKit.collectWindowErrors) { 132 | return; 133 | } 134 | for (var i in handlers) { 135 | if (_has(handlers, i)) { 136 | try { 137 | handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2))); 138 | } catch (inner) { 139 | exception = inner; 140 | } 141 | } 142 | } 143 | 144 | if (exception) { 145 | throw exception; 146 | } 147 | } 148 | 149 | var _oldOnerrorHandler, _onErrorHandlerInstalled; 150 | 151 | /** 152 | * Ensures all global unhandled exceptions are recorded. 153 | * Supported by Gecko and IE. 154 | * @param {string} message Error message. 155 | * @param {string} url URL of script that generated the exception. 156 | * @param {(number|string)} lineNo The line number at which the error 157 | * occurred. 158 | */ 159 | function traceKitWindowOnError(message, url, lineNo) { 160 | var stack = null; 161 | 162 | if (lastExceptionStack) { 163 | TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message); 164 | stack = lastExceptionStack; 165 | lastExceptionStack = null; 166 | lastException = null; 167 | } else { 168 | var location = { 169 | 'url': url, 170 | 'line': lineNo 171 | }; 172 | location.func = TraceKit.computeStackTrace.guessFunctionName(location.url, location.line); 173 | location.context = TraceKit.computeStackTrace.gatherContext(location.url, location.line); 174 | stack = { 175 | 'mode': 'onerror', 176 | 'message': message, 177 | 'url': document.location.href, 178 | 'stack': [location], 179 | 'useragent': navigator.userAgent 180 | }; 181 | } 182 | 183 | notifyHandlers(stack, 'from window.onerror'); 184 | 185 | if (_oldOnerrorHandler) { 186 | return _oldOnerrorHandler.apply(this, arguments); 187 | } 188 | 189 | return false; 190 | } 191 | 192 | function installGlobalHandler () 193 | { 194 | if (_onErrorHandlerInstalled === true) { 195 | return; 196 | } 197 | _oldOnerrorHandler = window.onerror; 198 | window.onerror = traceKitWindowOnError; 199 | _onErrorHandlerInstalled = true; 200 | } 201 | 202 | /** 203 | * Reports an unhandled Error to TraceKit. 204 | * @param {Error} ex 205 | */ 206 | function report(ex) { 207 | var args = _slice.call(arguments, 1); 208 | if (lastExceptionStack) { 209 | if (lastException === ex) { 210 | return; // already caught by an inner catch block, ignore 211 | } else { 212 | var s = lastExceptionStack; 213 | lastExceptionStack = null; 214 | lastException = null; 215 | notifyHandlers.apply(null, [s, null].concat(args)); 216 | } 217 | } 218 | 219 | var stack = TraceKit.computeStackTrace(ex); 220 | lastExceptionStack = stack; 221 | lastException = ex; 222 | 223 | // If the stack trace is incomplete, wait for 2 seconds for 224 | // slow slow IE to see if onerror occurs or not before reporting 225 | // this exception; otherwise, we will end up with an incomplete 226 | // stack trace 227 | window.setTimeout(function () { 228 | if (lastException === ex) { 229 | lastExceptionStack = null; 230 | lastException = null; 231 | notifyHandlers.apply(null, [stack, null].concat(args)); 232 | } 233 | }, (stack.incomplete ? 2000 : 0)); 234 | 235 | throw ex; // re-throw to propagate to the top level (and cause window.onerror) 236 | } 237 | 238 | report.subscribe = subscribe; 239 | report.unsubscribe = unsubscribe; 240 | return report; 241 | }()); 242 | 243 | /** 244 | * TraceKit.computeStackTrace: cross-browser stack traces in JavaScript 245 | * 246 | * Syntax: 247 | * s = TraceKit.computeStackTrace.ofCaller([depth]) 248 | * s = TraceKit.computeStackTrace(exception) // consider using TraceKit.report instead (see below) 249 | * Returns: 250 | * s.name - exception name 251 | * s.message - exception message 252 | * s.stack[i].url - JavaScript or HTML file URL 253 | * s.stack[i].func - function name, or empty for anonymous functions (if guessing did not work) 254 | * s.stack[i].args - arguments passed to the function, if known 255 | * s.stack[i].line - line number, if known 256 | * s.stack[i].column - column number, if known 257 | * s.stack[i].context - an array of source code lines; the middle element corresponds to the correct line# 258 | * s.mode - 'stack', 'stacktrace', 'multiline', 'callers', 'onerror', or 'failed' -- method used to collect the stack trace 259 | * 260 | * Supports: 261 | * - Firefox: full stack trace with line numbers and unreliable column 262 | * number on top frame 263 | * - Opera 10: full stack trace with line and column numbers 264 | * - Opera 9-: full stack trace with line numbers 265 | * - Chrome: full stack trace with line and column numbers 266 | * - Safari: line and column number for the topmost stacktrace element 267 | * only 268 | * - IE: no line numbers whatsoever 269 | * 270 | * Tries to guess names of anonymous functions by looking for assignments 271 | * in the source code. In IE and Safari, we have to guess source file names 272 | * by searching for function bodies inside all page scripts. This will not 273 | * work for scripts that are loaded cross-domain. 274 | * Here be dragons: some function names may be guessed incorrectly, and 275 | * duplicate functions may be mismatched. 276 | * 277 | * TraceKit.computeStackTrace should only be used for tracing purposes. 278 | * Logging of unhandled exceptions should be done with TraceKit.report, 279 | * which builds on top of TraceKit.computeStackTrace and provides better 280 | * IE support by utilizing the window.onerror event to retrieve information 281 | * about the top of the stack. 282 | * 283 | * Note: In IE and Safari, no stack trace is recorded on the Error object, 284 | * so computeStackTrace instead walks its *own* chain of callers. 285 | * This means that: 286 | * * in Safari, some methods may be missing from the stack trace; 287 | * * in IE, the topmost function in the stack trace will always be the 288 | * caller of computeStackTrace. 289 | * 290 | * This is okay for tracing (because you are likely to be calling 291 | * computeStackTrace from the function you want to be the topmost element 292 | * of the stack trace anyway), but not okay for logging unhandled 293 | * exceptions (because your catch block will likely be far away from the 294 | * inner function that actually caused the exception). 295 | * 296 | * Tracing example: 297 | * function trace(message) { 298 | * var stackInfo = TraceKit.computeStackTrace.ofCaller(); 299 | * var data = message + "\n"; 300 | * for(var i in stackInfo.stack) { 301 | * var item = stackInfo.stack[i]; 302 | * data += (item.func || '[anonymous]') + "() in " + item.url + ":" + (item.line || '0') + "\n"; 303 | * } 304 | * if (window.console) 305 | * console.info(data); 306 | * else 307 | * alert(data); 308 | * } 309 | */ 310 | TraceKit.computeStackTrace = (function computeStackTraceWrapper() { 311 | var debug = false, 312 | sourceCache = {}; 313 | 314 | /** 315 | * Attempts to retrieve source code via XMLHttpRequest, which is used 316 | * to look up anonymous function names. 317 | * @param {string} url URL of source code. 318 | * @return {string} Source contents. 319 | */ 320 | function loadSource(url) { 321 | if (!TraceKit.remoteFetching) { //Only attempt request if remoteFetching is on. 322 | return ''; 323 | } 324 | try { 325 | var getXHR = function() { 326 | try { 327 | return new window.XMLHttpRequest(); 328 | } catch (e) { 329 | // explicitly bubble up the exception if not found 330 | return new window.ActiveXObject('Microsoft.XMLHTTP'); 331 | } 332 | }; 333 | 334 | var request = getXHR(); 335 | request.open('GET', url, false); 336 | request.send(''); 337 | return request.responseText; 338 | } catch (e) { 339 | return ''; 340 | } 341 | } 342 | 343 | /** 344 | * Retrieves source code from the source code cache. 345 | * @param {string} url URL of source code. 346 | * @return {Array.} Source contents. 347 | */ 348 | function getSource(url) { 349 | if (!_has(sourceCache, url)) { 350 | // URL needs to be able to fetched within the acceptable domain. Otherwise, 351 | // cross-domain errors will be triggered. 352 | var source = ''; 353 | if (url.indexOf(document.domain) !== -1) { 354 | source = loadSource(url); 355 | } 356 | sourceCache[url] = source ? source.split('\n') : []; 357 | } 358 | 359 | return sourceCache[url]; 360 | } 361 | 362 | /** 363 | * Tries to use an externally loaded copy of source code to determine 364 | * the name of a function by looking at the name of the variable it was 365 | * assigned to, if any. 366 | * @param {string} url URL of source code. 367 | * @param {(string|number)} lineNo Line number in source code. 368 | * @return {string} The function name, if discoverable. 369 | */ 370 | function guessFunctionName(url, lineNo) { 371 | var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/, 372 | reGuessFunction = /['"]?([0-9A-Za-z$_]+)['"]?\s*[:=]\s*(function|eval|new Function)/, 373 | line = '', 374 | maxLines = 10, 375 | source = getSource(url), 376 | m; 377 | 378 | if (!source.length) { 379 | return UNKNOWN_FUNCTION; 380 | } 381 | 382 | // Walk backwards from the first line in the function until we find the line which 383 | // matches the pattern above, which is the function definition 384 | for (var i = 0; i < maxLines; ++i) { 385 | line = source[lineNo - i] + line; 386 | 387 | if (!_isUndefined(line)) { 388 | if ((m = reGuessFunction.exec(line))) { 389 | return m[1]; 390 | } else if ((m = reFunctionArgNames.exec(line))) { 391 | return m[1]; 392 | } 393 | } 394 | } 395 | 396 | return UNKNOWN_FUNCTION; 397 | } 398 | 399 | /** 400 | * Retrieves the surrounding lines from where an exception occurred. 401 | * @param {string} url URL of source code. 402 | * @param {(string|number)} line Line number in source code to centre 403 | * around for context. 404 | * @return {?Array.} Lines of source code. 405 | */ 406 | function gatherContext(url, line) { 407 | var source = getSource(url); 408 | 409 | if (!source.length) { 410 | return null; 411 | } 412 | 413 | var context = [], 414 | // linesBefore & linesAfter are inclusive with the offending line. 415 | // if linesOfContext is even, there will be one extra line 416 | // *before* the offending line. 417 | linesBefore = Math.floor(TraceKit.linesOfContext / 2), 418 | // Add one extra line if linesOfContext is odd 419 | linesAfter = linesBefore + (TraceKit.linesOfContext % 2), 420 | start = Math.max(0, line - linesBefore - 1), 421 | end = Math.min(source.length, line + linesAfter - 1); 422 | 423 | line -= 1; // convert to 0-based index 424 | 425 | for (var i = start; i < end; ++i) { 426 | if (!_isUndefined(source[i])) { 427 | context.push(source[i]); 428 | } 429 | } 430 | 431 | return context.length > 0 ? context : null; 432 | } 433 | 434 | /** 435 | * Escapes special characters, except for whitespace, in a string to be 436 | * used inside a regular expression as a string literal. 437 | * @param {string} text The string. 438 | * @return {string} The escaped string literal. 439 | */ 440 | function escapeRegExp(text) { 441 | return text.replace(/[\-\[\]{}()*+?.,\\\^$|#]/g, '\\$&'); 442 | } 443 | 444 | /** 445 | * Escapes special characters in a string to be used inside a regular 446 | * expression as a string literal. Also ensures that HTML entities will 447 | * be matched the same as their literal friends. 448 | * @param {string} body The string. 449 | * @return {string} The escaped string. 450 | */ 451 | function escapeCodeAsRegExpForMatchingInsideHTML(body) { 452 | return escapeRegExp(body).replace('<', '(?:<|<)').replace('>', '(?:>|>)').replace('&', '(?:&|&)').replace('"', '(?:"|")').replace(/\s+/g, '\\s+'); 453 | } 454 | 455 | /** 456 | * Determines where a code fragment occurs in the source code. 457 | * @param {RegExp} re The function definition. 458 | * @param {Array.} urls A list of URLs to search. 459 | * @return {?Object.} An object containing 460 | * the url, line, and column number of the defined function. 461 | */ 462 | function findSourceInUrls(re, urls) { 463 | var source, m; 464 | for (var i = 0, j = urls.length; i < j; ++i) { 465 | // console.log('searching', urls[i]); 466 | if ((source = getSource(urls[i])).length) { 467 | source = source.join('\n'); 468 | if ((m = re.exec(source))) { 469 | // console.log('Found function in ' + urls[i]); 470 | 471 | return { 472 | 'url': urls[i], 473 | 'line': source.substring(0, m.index).split('\n').length, 474 | 'column': m.index - source.lastIndexOf('\n', m.index) - 1 475 | }; 476 | } 477 | } 478 | } 479 | 480 | // console.log('no match'); 481 | 482 | return null; 483 | } 484 | 485 | /** 486 | * Determines at which column a code fragment occurs on a line of the 487 | * source code. 488 | * @param {string} fragment The code fragment. 489 | * @param {string} url The URL to search. 490 | * @param {(string|number)} line The line number to examine. 491 | * @return {?number} The column number. 492 | */ 493 | function findSourceInLine(fragment, url, line) { 494 | var source = getSource(url), 495 | re = new RegExp('\\b' + escapeRegExp(fragment) + '\\b'), 496 | m; 497 | 498 | line -= 1; 499 | 500 | if (source && source.length > line && (m = re.exec(source[line]))) { 501 | return m.index; 502 | } 503 | 504 | return null; 505 | } 506 | 507 | /** 508 | * Determines where a function was defined within the source code. 509 | * @param {(Function|string)} func A function reference or serialized 510 | * function definition. 511 | * @return {?Object.} An object containing 512 | * the url, line, and column number of the defined function. 513 | */ 514 | function findSourceByFunctionBody(func) { 515 | var urls = [window.location.href], 516 | scripts = document.getElementsByTagName('script'), 517 | body, 518 | code = '' + func, 519 | codeRE = /^function(?:\s+([\w$]+))?\s*\(([\w\s,]*)\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, 520 | eventRE = /^function on([\w$]+)\s*\(event\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, 521 | re, 522 | parts, 523 | result; 524 | 525 | for (var i = 0; i < scripts.length; ++i) { 526 | var script = scripts[i]; 527 | if (script.src) { 528 | urls.push(script.src); 529 | } 530 | } 531 | 532 | if (!(parts = codeRE.exec(code))) { 533 | re = new RegExp(escapeRegExp(code).replace(/\s+/g, '\\s+')); 534 | } 535 | 536 | // not sure if this is really necessary, but I don’t have a test 537 | // corpus large enough to confirm that and it was in the original. 538 | else { 539 | var name = parts[1] ? '\\s+' + parts[1] : '', 540 | args = parts[2].split(',').join('\\s*,\\s*'); 541 | 542 | body = escapeRegExp(parts[3]).replace(/;$/, ';?'); // semicolon is inserted if the function ends with a comment.replace(/\s+/g, '\\s+'); 543 | re = new RegExp('function' + name + '\\s*\\(\\s*' + args + '\\s*\\)\\s*{\\s*' + body + '\\s*}'); 544 | } 545 | 546 | // look for a normal function definition 547 | if ((result = findSourceInUrls(re, urls))) { 548 | return result; 549 | } 550 | 551 | // look for an old-school event handler function 552 | if ((parts = eventRE.exec(code))) { 553 | var event = parts[1]; 554 | body = escapeCodeAsRegExpForMatchingInsideHTML(parts[2]); 555 | 556 | // look for a function defined in HTML as an onXXX handler 557 | re = new RegExp('on' + event + '=[\\\'"]\\s*' + body + '\\s*[\\\'"]', 'i'); 558 | 559 | if ((result = findSourceInUrls(re, urls[0]))) { 560 | return result; 561 | } 562 | 563 | // look for ??? 564 | re = new RegExp(body); 565 | 566 | if ((result = findSourceInUrls(re, urls))) { 567 | return result; 568 | } 569 | } 570 | 571 | return null; 572 | } 573 | 574 | // Contents of Exception in various browsers. 575 | // 576 | // SAFARI: 577 | // ex.message = Can't find variable: qq 578 | // ex.line = 59 579 | // ex.sourceId = 580238192 580 | // ex.sourceURL = http://... 581 | // ex.expressionBeginOffset = 96 582 | // ex.expressionCaretOffset = 98 583 | // ex.expressionEndOffset = 98 584 | // ex.name = ReferenceError 585 | // 586 | // FIREFOX: 587 | // ex.message = qq is not defined 588 | // ex.fileName = http://... 589 | // ex.lineNumber = 59 590 | // ex.stack = ...stack trace... (see the example below) 591 | // ex.name = ReferenceError 592 | // 593 | // CHROME: 594 | // ex.message = qq is not defined 595 | // ex.name = ReferenceError 596 | // ex.type = not_defined 597 | // ex.arguments = ['aa'] 598 | // ex.stack = ...stack trace... 599 | // 600 | // INTERNET EXPLORER: 601 | // ex.message = ... 602 | // ex.name = ReferenceError 603 | // 604 | // OPERA: 605 | // ex.message = ...message... (see the example below) 606 | // ex.name = ReferenceError 607 | // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message) 608 | // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace' 609 | 610 | /** 611 | * Computes stack trace information from the stack property. 612 | * Chrome and Gecko use this property. 613 | * @param {Error} ex 614 | * @return {?Object.} Stack trace information. 615 | */ 616 | function computeStackTraceFromStackProp(ex) { 617 | if (!ex.stack) { 618 | return null; 619 | } 620 | 621 | var chrome = /^\s*at (?:((?:\[object object\])?\S+(?: \[as \S+\])?) )?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$/i, 622 | gecko = /^\s*(\S*)(?:\((.*?)\))?@((?:file|http|https).*?):(\d+)(?::(\d+))?\s*$/i, 623 | lines = ex.stack.split('\n'), 624 | stack = [], 625 | parts, 626 | element, 627 | reference = /^(.*) is undefined$/.exec(ex.message); 628 | 629 | for (var i = 0, j = lines.length; i < j; ++i) { 630 | if ((parts = gecko.exec(lines[i]))) { 631 | element = { 632 | 'url': parts[3], 633 | 'func': parts[1] || UNKNOWN_FUNCTION, 634 | 'args': parts[2] ? parts[2].split(',') : '', 635 | 'line': +parts[4], 636 | 'column': parts[5] ? +parts[5] : null 637 | }; 638 | } else if ((parts = chrome.exec(lines[i]))) { 639 | element = { 640 | 'url': parts[2], 641 | 'func': parts[1] || UNKNOWN_FUNCTION, 642 | 'line': +parts[3], 643 | 'column': parts[4] ? +parts[4] : null 644 | }; 645 | } else { 646 | continue; 647 | } 648 | 649 | if (!element.func && element.line) { 650 | element.func = guessFunctionName(element.url, element.line); 651 | } 652 | 653 | if (element.line) { 654 | element.context = gatherContext(element.url, element.line); 655 | } 656 | 657 | stack.push(element); 658 | } 659 | 660 | if (stack[0] && stack[0].line && !stack[0].column && reference) { 661 | stack[0].column = findSourceInLine(reference[1], stack[0].url, stack[0].line); 662 | } 663 | 664 | if (!stack.length) { 665 | return null; 666 | } 667 | 668 | return { 669 | 'mode': 'stack', 670 | 'name': ex.name, 671 | 'message': ex.message, 672 | 'url': document.location.href, 673 | 'stack': stack, 674 | 'useragent': navigator.userAgent 675 | }; 676 | } 677 | 678 | /** 679 | * Computes stack trace information from the stacktrace property. 680 | * Opera 10 uses this property. 681 | * @param {Error} ex 682 | * @return {?Object.} Stack trace information. 683 | */ 684 | function computeStackTraceFromStacktraceProp(ex) { 685 | // Access and store the stacktrace property before doing ANYTHING 686 | // else to it because Opera is not very good at providing it 687 | // reliably in other circumstances. 688 | var stacktrace = ex.stacktrace; 689 | 690 | var testRE = / line (\d+), column (\d+) in (?:]+)>|([^\)]+))\((.*)\) in (.*):\s*$/i, 691 | lines = stacktrace.split('\n'), 692 | stack = [], 693 | parts; 694 | 695 | for (var i = 0, j = lines.length; i < j; i += 2) { 696 | if ((parts = testRE.exec(lines[i]))) { 697 | var element = { 698 | 'line': +parts[1], 699 | 'column': +parts[2], 700 | 'func': parts[3] || parts[4], 701 | 'args': parts[5] ? parts[5].split(',') : [], 702 | 'url': parts[6] 703 | }; 704 | 705 | if (!element.func && element.line) { 706 | element.func = guessFunctionName(element.url, element.line); 707 | } 708 | if (element.line) { 709 | try { 710 | element.context = gatherContext(element.url, element.line); 711 | } catch (exc) {} 712 | } 713 | 714 | if (!element.context) { 715 | element.context = [lines[i + 1]]; 716 | } 717 | 718 | stack.push(element); 719 | } 720 | } 721 | 722 | if (!stack.length) { 723 | return null; 724 | } 725 | 726 | return { 727 | 'mode': 'stacktrace', 728 | 'name': ex.name, 729 | 'message': ex.message, 730 | 'url': document.location.href, 731 | 'stack': stack, 732 | 'useragent': navigator.userAgent 733 | }; 734 | } 735 | 736 | /** 737 | * NOT TESTED. 738 | * Computes stack trace information from an error message that includes 739 | * the stack trace. 740 | * Opera 9 and earlier use this method if the option to show stack 741 | * traces is turned on in opera:config. 742 | * @param {Error} ex 743 | * @return {?Object.} Stack information. 744 | */ 745 | function computeStackTraceFromOperaMultiLineMessage(ex) { 746 | // Opera includes a stack trace into the exception message. An example is: 747 | // 748 | // Statement on line 3: Undefined variable: undefinedFunc 749 | // Backtrace: 750 | // Line 3 of linked script file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.js: In function zzz 751 | // undefinedFunc(a); 752 | // Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function yyy 753 | // zzz(x, y, z); 754 | // Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function xxx 755 | // yyy(a, a, a); 756 | // Line 1 of function script 757 | // try { xxx('hi'); return false; } catch(ex) { TraceKit.report(ex); } 758 | // ... 759 | 760 | var lines = ex.message.split('\n'); 761 | if (lines.length < 4) { 762 | return null; 763 | } 764 | 765 | var lineRE1 = /^\s*Line (\d+) of linked script ((?:file|http|https)\S+)(?:: in function (\S+))?\s*$/i, 766 | lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|http|https)\S+)(?:: in function (\S+))?\s*$/i, 767 | lineRE3 = /^\s*Line (\d+) of function script\s*$/i, 768 | stack = [], 769 | scripts = document.getElementsByTagName('script'), 770 | inlineScriptBlocks = [], 771 | parts, 772 | i, 773 | len, 774 | source; 775 | 776 | for (i in scripts) { 777 | if (_has(scripts, i) && !scripts[i].src) { 778 | inlineScriptBlocks.push(scripts[i]); 779 | } 780 | } 781 | 782 | for (i = 2, len = lines.length; i < len; i += 2) { 783 | var item = null; 784 | if ((parts = lineRE1.exec(lines[i]))) { 785 | item = { 786 | 'url': parts[2], 787 | 'func': parts[3], 788 | 'line': +parts[1] 789 | }; 790 | } else if ((parts = lineRE2.exec(lines[i]))) { 791 | item = { 792 | 'url': parts[3], 793 | 'func': parts[4] 794 | }; 795 | var relativeLine = (+parts[1]); // relative to the start of the 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var livereload = require('gulp-livereload'); 3 | var serve = require('gulp-serve'); 4 | var concat = require('gulp-concat'); 5 | var uglify = require('gulp-uglify'); 6 | 7 | // Static file server 8 | gulp.task('server', serve({ 9 | root: ['example', 'dist'], 10 | port: 7000 11 | })); 12 | 13 | // Task for refreshing everything (HTML, etc) 14 | gulp.task('refresh-browser', function() { 15 | gulp.src('package.json', {read: false}) 16 | .pipe(livereload()); 17 | }); 18 | 19 | // Files to bundle 20 | JS_DIST_FILES = [ 21 | 'libs/tracekit.js', 22 | 'src/opbeat.js' 23 | ]; 24 | 25 | // Bundles files into 26 | gulp.task('process-scripts', function() { 27 | gulp.src(JS_DIST_FILES) 28 | .pipe(concat('opbeat.js')) 29 | .pipe(gulp.dest('./dist')) 30 | .pipe(uglify().on('error', function(e) { console.log('\x07',e.message); return this.end(); })) 31 | .pipe(concat('opbeat.min.js')) 32 | .pipe(gulp.dest('./dist')) 33 | .pipe(livereload()); 34 | }); 35 | 36 | // Development mode 37 | gulp.task('watch', [], function(cb){ 38 | 39 | // Livereload 40 | livereload.listen(); 41 | 42 | gulp.run( 43 | 'process-scripts', 44 | 'server' 45 | ); 46 | 47 | // Watch JS files 48 | gulp.watch(['libs/**', 'src/**'], ['process-scripts']); 49 | 50 | // Watch example files 51 | gulp.watch('example/**', ['refresh-browser']); 52 | 53 | console.log('\nExample site running on http://localhost:7000/\n'); 54 | }); 55 | 56 | 57 | // 58 | // Default task 59 | // 60 | gulp.task('default', function(){ 61 | var response = ['', 62 | 'No task selected.', 63 | 'Available tasks:', '', 64 | 'gulp watch - Watch files and preview example site on localhost.', '' 65 | ].join('\n'); 66 | 67 | console.log(response); 68 | }); 69 | -------------------------------------------------------------------------------- /libs/tracekit.js: -------------------------------------------------------------------------------- 1 | /* 2 | TraceKit - Cross brower stack traces - github.com/occ/TraceKit 3 | MIT license 4 | */ 5 | 6 | (function(window, undefined) { 7 | 8 | 9 | var TraceKit = {}; 10 | var _oldTraceKit = window.TraceKit; 11 | 12 | // global reference to slice 13 | var _slice = [].slice; 14 | var UNKNOWN_FUNCTION = '?'; 15 | 16 | 17 | /** 18 | * _has, a better form of hasOwnProperty 19 | * Example: _has(MainHostObject, property) === true/false 20 | * 21 | * @param {Object} host object to check property 22 | * @param {string} key to check 23 | */ 24 | function _has(object, key) { 25 | return Object.prototype.hasOwnProperty.call(object, key); 26 | } 27 | 28 | function _isUndefined(what) { 29 | return typeof what === 'undefined'; 30 | } 31 | 32 | /** 33 | * TraceKit.noConflict: Export TraceKit out to another variable 34 | * Example: var TK = TraceKit.noConflict() 35 | */ 36 | TraceKit.noConflict = function noConflict() { 37 | window.TraceKit = _oldTraceKit; 38 | return TraceKit; 39 | }; 40 | 41 | /** 42 | * TraceKit.wrap: Wrap any function in a TraceKit reporter 43 | * Example: func = TraceKit.wrap(func); 44 | * 45 | * @param {Function} func Function to be wrapped 46 | * @return {Function} The wrapped func 47 | */ 48 | TraceKit.wrap = function traceKitWrapper(func) { 49 | function wrapped() { 50 | try { 51 | return func.apply(this, arguments); 52 | } catch (e) { 53 | TraceKit.report(e); 54 | throw e; 55 | } 56 | } 57 | return wrapped; 58 | }; 59 | 60 | /** 61 | * TraceKit.report: cross-browser processing of unhandled exceptions 62 | * 63 | * Syntax: 64 | * TraceKit.report.subscribe(function(stackInfo) { ... }) 65 | * TraceKit.report.unsubscribe(function(stackInfo) { ... }) 66 | * TraceKit.report(exception) 67 | * try { ...code... } catch(ex) { TraceKit.report(ex); } 68 | * 69 | * Supports: 70 | * - Firefox: full stack trace with line numbers, plus column number 71 | * on top frame; column number is not guaranteed 72 | * - Opera: full stack trace with line and column numbers 73 | * - Chrome: full stack trace with line and column numbers 74 | * - Safari: line and column number for the top frame only; some frames 75 | * may be missing, and column number is not guaranteed 76 | * - IE: line and column number for the top frame only; some frames 77 | * may be missing, and column number is not guaranteed 78 | * 79 | * In theory, TraceKit should work on all of the following versions: 80 | * - IE5.5+ (only 8.0 tested) 81 | * - Firefox 0.9+ (only 3.5+ tested) 82 | * - Opera 7+ (only 10.50 tested; versions 9 and earlier may require 83 | * Exceptions Have Stacktrace to be enabled in opera:config) 84 | * - Safari 3+ (only 4+ tested) 85 | * - Chrome 1+ (only 5+ tested) 86 | * - Konqueror 3.5+ (untested) 87 | * 88 | * Requires TraceKit.computeStackTrace. 89 | * 90 | * Tries to catch all unhandled exceptions and report them to the 91 | * subscribed handlers. Please note that TraceKit.report will rethrow the 92 | * exception. This is REQUIRED in order to get a useful stack trace in IE. 93 | * If the exception does not reach the top of the browser, you will only 94 | * get a stack trace from the point where TraceKit.report was called. 95 | * 96 | * Handlers receive a stackInfo object as described in the 97 | * TraceKit.computeStackTrace docs. 98 | */ 99 | TraceKit.report = (function reportModuleWrapper() { 100 | var handlers = [], 101 | lastException = null, 102 | lastExceptionStack = null; 103 | 104 | /** 105 | * Add a crash handler. 106 | * @param {Function} handler 107 | */ 108 | function subscribe(handler) { 109 | installGlobalHandler(); 110 | handlers.push(handler); 111 | } 112 | 113 | /** 114 | * Remove a crash handler. 115 | * @param {Function} handler 116 | */ 117 | function unsubscribe(handler) { 118 | for (var i = handlers.length - 1; i >= 0; --i) { 119 | if (handlers[i] === handler) { 120 | handlers.splice(i, 1); 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Dispatch stack information to all handlers. 127 | * @param {Object.} stack 128 | */ 129 | function notifyHandlers(stack, windowError) { 130 | var exception = null; 131 | if (windowError && !TraceKit.collectWindowErrors) { 132 | return; 133 | } 134 | for (var i in handlers) { 135 | if (_has(handlers, i)) { 136 | try { 137 | handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2))); 138 | } catch (inner) { 139 | exception = inner; 140 | } 141 | } 142 | } 143 | 144 | if (exception) { 145 | throw exception; 146 | } 147 | } 148 | 149 | var _oldOnerrorHandler, _onErrorHandlerInstalled; 150 | 151 | /** 152 | * Ensures all global unhandled exceptions are recorded. 153 | * Supported by Gecko and IE. 154 | * @param {string} message Error message. 155 | * @param {string} url URL of script that generated the exception. 156 | * @param {(number|string)} lineNo The line number at which the error 157 | * occurred. 158 | */ 159 | function traceKitWindowOnError(message, url, lineNo) { 160 | var stack = null; 161 | 162 | if (lastExceptionStack) { 163 | TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message); 164 | stack = lastExceptionStack; 165 | lastExceptionStack = null; 166 | lastException = null; 167 | } else { 168 | var location = { 169 | 'url': url, 170 | 'line': lineNo 171 | }; 172 | location.func = TraceKit.computeStackTrace.guessFunctionName(location.url, location.line); 173 | location.context = TraceKit.computeStackTrace.gatherContext(location.url, location.line); 174 | stack = { 175 | 'mode': 'onerror', 176 | 'message': message, 177 | 'url': document.location.href, 178 | 'stack': [location], 179 | 'useragent': navigator.userAgent 180 | }; 181 | } 182 | 183 | notifyHandlers(stack, 'from window.onerror'); 184 | 185 | if (_oldOnerrorHandler) { 186 | return _oldOnerrorHandler.apply(this, arguments); 187 | } 188 | 189 | return false; 190 | } 191 | 192 | function installGlobalHandler () 193 | { 194 | if (_onErrorHandlerInstalled === true) { 195 | return; 196 | } 197 | _oldOnerrorHandler = window.onerror; 198 | window.onerror = traceKitWindowOnError; 199 | _onErrorHandlerInstalled = true; 200 | } 201 | 202 | /** 203 | * Reports an unhandled Error to TraceKit. 204 | * @param {Error} ex 205 | */ 206 | function report(ex) { 207 | var args = _slice.call(arguments, 1); 208 | if (lastExceptionStack) { 209 | if (lastException === ex) { 210 | return; // already caught by an inner catch block, ignore 211 | } else { 212 | var s = lastExceptionStack; 213 | lastExceptionStack = null; 214 | lastException = null; 215 | notifyHandlers.apply(null, [s, null].concat(args)); 216 | } 217 | } 218 | 219 | var stack = TraceKit.computeStackTrace(ex); 220 | lastExceptionStack = stack; 221 | lastException = ex; 222 | 223 | // If the stack trace is incomplete, wait for 2 seconds for 224 | // slow slow IE to see if onerror occurs or not before reporting 225 | // this exception; otherwise, we will end up with an incomplete 226 | // stack trace 227 | window.setTimeout(function () { 228 | if (lastException === ex) { 229 | lastExceptionStack = null; 230 | lastException = null; 231 | notifyHandlers.apply(null, [stack, null].concat(args)); 232 | } 233 | }, (stack.incomplete ? 2000 : 0)); 234 | 235 | throw ex; // re-throw to propagate to the top level (and cause window.onerror) 236 | } 237 | 238 | report.subscribe = subscribe; 239 | report.unsubscribe = unsubscribe; 240 | return report; 241 | }()); 242 | 243 | /** 244 | * TraceKit.computeStackTrace: cross-browser stack traces in JavaScript 245 | * 246 | * Syntax: 247 | * s = TraceKit.computeStackTrace.ofCaller([depth]) 248 | * s = TraceKit.computeStackTrace(exception) // consider using TraceKit.report instead (see below) 249 | * Returns: 250 | * s.name - exception name 251 | * s.message - exception message 252 | * s.stack[i].url - JavaScript or HTML file URL 253 | * s.stack[i].func - function name, or empty for anonymous functions (if guessing did not work) 254 | * s.stack[i].args - arguments passed to the function, if known 255 | * s.stack[i].line - line number, if known 256 | * s.stack[i].column - column number, if known 257 | * s.stack[i].context - an array of source code lines; the middle element corresponds to the correct line# 258 | * s.mode - 'stack', 'stacktrace', 'multiline', 'callers', 'onerror', or 'failed' -- method used to collect the stack trace 259 | * 260 | * Supports: 261 | * - Firefox: full stack trace with line numbers and unreliable column 262 | * number on top frame 263 | * - Opera 10: full stack trace with line and column numbers 264 | * - Opera 9-: full stack trace with line numbers 265 | * - Chrome: full stack trace with line and column numbers 266 | * - Safari: line and column number for the topmost stacktrace element 267 | * only 268 | * - IE: no line numbers whatsoever 269 | * 270 | * Tries to guess names of anonymous functions by looking for assignments 271 | * in the source code. In IE and Safari, we have to guess source file names 272 | * by searching for function bodies inside all page scripts. This will not 273 | * work for scripts that are loaded cross-domain. 274 | * Here be dragons: some function names may be guessed incorrectly, and 275 | * duplicate functions may be mismatched. 276 | * 277 | * TraceKit.computeStackTrace should only be used for tracing purposes. 278 | * Logging of unhandled exceptions should be done with TraceKit.report, 279 | * which builds on top of TraceKit.computeStackTrace and provides better 280 | * IE support by utilizing the window.onerror event to retrieve information 281 | * about the top of the stack. 282 | * 283 | * Note: In IE and Safari, no stack trace is recorded on the Error object, 284 | * so computeStackTrace instead walks its *own* chain of callers. 285 | * This means that: 286 | * * in Safari, some methods may be missing from the stack trace; 287 | * * in IE, the topmost function in the stack trace will always be the 288 | * caller of computeStackTrace. 289 | * 290 | * This is okay for tracing (because you are likely to be calling 291 | * computeStackTrace from the function you want to be the topmost element 292 | * of the stack trace anyway), but not okay for logging unhandled 293 | * exceptions (because your catch block will likely be far away from the 294 | * inner function that actually caused the exception). 295 | * 296 | * Tracing example: 297 | * function trace(message) { 298 | * var stackInfo = TraceKit.computeStackTrace.ofCaller(); 299 | * var data = message + "\n"; 300 | * for(var i in stackInfo.stack) { 301 | * var item = stackInfo.stack[i]; 302 | * data += (item.func || '[anonymous]') + "() in " + item.url + ":" + (item.line || '0') + "\n"; 303 | * } 304 | * if (window.console) 305 | * console.info(data); 306 | * else 307 | * alert(data); 308 | * } 309 | */ 310 | TraceKit.computeStackTrace = (function computeStackTraceWrapper() { 311 | var debug = false, 312 | sourceCache = {}; 313 | 314 | /** 315 | * Attempts to retrieve source code via XMLHttpRequest, which is used 316 | * to look up anonymous function names. 317 | * @param {string} url URL of source code. 318 | * @return {string} Source contents. 319 | */ 320 | function loadSource(url) { 321 | if (!TraceKit.remoteFetching) { //Only attempt request if remoteFetching is on. 322 | return ''; 323 | } 324 | try { 325 | var getXHR = function() { 326 | try { 327 | return new window.XMLHttpRequest(); 328 | } catch (e) { 329 | // explicitly bubble up the exception if not found 330 | return new window.ActiveXObject('Microsoft.XMLHTTP'); 331 | } 332 | }; 333 | 334 | var request = getXHR(); 335 | request.open('GET', url, false); 336 | request.send(''); 337 | return request.responseText; 338 | } catch (e) { 339 | return ''; 340 | } 341 | } 342 | 343 | /** 344 | * Retrieves source code from the source code cache. 345 | * @param {string} url URL of source code. 346 | * @return {Array.} Source contents. 347 | */ 348 | function getSource(url) { 349 | if (!_has(sourceCache, url)) { 350 | // URL needs to be able to fetched within the acceptable domain. Otherwise, 351 | // cross-domain errors will be triggered. 352 | var source = ''; 353 | if (url.indexOf(document.domain) !== -1) { 354 | source = loadSource(url); 355 | } 356 | sourceCache[url] = source ? source.split('\n') : []; 357 | } 358 | 359 | return sourceCache[url]; 360 | } 361 | 362 | /** 363 | * Tries to use an externally loaded copy of source code to determine 364 | * the name of a function by looking at the name of the variable it was 365 | * assigned to, if any. 366 | * @param {string} url URL of source code. 367 | * @param {(string|number)} lineNo Line number in source code. 368 | * @return {string} The function name, if discoverable. 369 | */ 370 | function guessFunctionName(url, lineNo) { 371 | var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/, 372 | reGuessFunction = /['"]?([0-9A-Za-z$_]+)['"]?\s*[:=]\s*(function|eval|new Function)/, 373 | line = '', 374 | maxLines = 10, 375 | source = getSource(url), 376 | m; 377 | 378 | if (!source.length) { 379 | return UNKNOWN_FUNCTION; 380 | } 381 | 382 | // Walk backwards from the first line in the function until we find the line which 383 | // matches the pattern above, which is the function definition 384 | for (var i = 0; i < maxLines; ++i) { 385 | line = source[lineNo - i] + line; 386 | 387 | if (!_isUndefined(line)) { 388 | if ((m = reGuessFunction.exec(line))) { 389 | return m[1]; 390 | } else if ((m = reFunctionArgNames.exec(line))) { 391 | return m[1]; 392 | } 393 | } 394 | } 395 | 396 | return UNKNOWN_FUNCTION; 397 | } 398 | 399 | /** 400 | * Retrieves the surrounding lines from where an exception occurred. 401 | * @param {string} url URL of source code. 402 | * @param {(string|number)} line Line number in source code to centre 403 | * around for context. 404 | * @return {?Array.} Lines of source code. 405 | */ 406 | function gatherContext(url, line) { 407 | var source = getSource(url); 408 | 409 | if (!source.length) { 410 | return null; 411 | } 412 | 413 | var context = [], 414 | // linesBefore & linesAfter are inclusive with the offending line. 415 | // if linesOfContext is even, there will be one extra line 416 | // *before* the offending line. 417 | linesBefore = Math.floor(TraceKit.linesOfContext / 2), 418 | // Add one extra line if linesOfContext is odd 419 | linesAfter = linesBefore + (TraceKit.linesOfContext % 2), 420 | start = Math.max(0, line - linesBefore - 1), 421 | end = Math.min(source.length, line + linesAfter - 1); 422 | 423 | line -= 1; // convert to 0-based index 424 | 425 | for (var i = start; i < end; ++i) { 426 | if (!_isUndefined(source[i])) { 427 | context.push(source[i]); 428 | } 429 | } 430 | 431 | return context.length > 0 ? context : null; 432 | } 433 | 434 | /** 435 | * Escapes special characters, except for whitespace, in a string to be 436 | * used inside a regular expression as a string literal. 437 | * @param {string} text The string. 438 | * @return {string} The escaped string literal. 439 | */ 440 | function escapeRegExp(text) { 441 | return text.replace(/[\-\[\]{}()*+?.,\\\^$|#]/g, '\\$&'); 442 | } 443 | 444 | /** 445 | * Escapes special characters in a string to be used inside a regular 446 | * expression as a string literal. Also ensures that HTML entities will 447 | * be matched the same as their literal friends. 448 | * @param {string} body The string. 449 | * @return {string} The escaped string. 450 | */ 451 | function escapeCodeAsRegExpForMatchingInsideHTML(body) { 452 | return escapeRegExp(body).replace('<', '(?:<|<)').replace('>', '(?:>|>)').replace('&', '(?:&|&)').replace('"', '(?:"|")').replace(/\s+/g, '\\s+'); 453 | } 454 | 455 | /** 456 | * Determines where a code fragment occurs in the source code. 457 | * @param {RegExp} re The function definition. 458 | * @param {Array.} urls A list of URLs to search. 459 | * @return {?Object.} An object containing 460 | * the url, line, and column number of the defined function. 461 | */ 462 | function findSourceInUrls(re, urls) { 463 | var source, m; 464 | for (var i = 0, j = urls.length; i < j; ++i) { 465 | // console.log('searching', urls[i]); 466 | if ((source = getSource(urls[i])).length) { 467 | source = source.join('\n'); 468 | if ((m = re.exec(source))) { 469 | // console.log('Found function in ' + urls[i]); 470 | 471 | return { 472 | 'url': urls[i], 473 | 'line': source.substring(0, m.index).split('\n').length, 474 | 'column': m.index - source.lastIndexOf('\n', m.index) - 1 475 | }; 476 | } 477 | } 478 | } 479 | 480 | // console.log('no match'); 481 | 482 | return null; 483 | } 484 | 485 | /** 486 | * Determines at which column a code fragment occurs on a line of the 487 | * source code. 488 | * @param {string} fragment The code fragment. 489 | * @param {string} url The URL to search. 490 | * @param {(string|number)} line The line number to examine. 491 | * @return {?number} The column number. 492 | */ 493 | function findSourceInLine(fragment, url, line) { 494 | var source = getSource(url), 495 | re = new RegExp('\\b' + escapeRegExp(fragment) + '\\b'), 496 | m; 497 | 498 | line -= 1; 499 | 500 | if (source && source.length > line && (m = re.exec(source[line]))) { 501 | return m.index; 502 | } 503 | 504 | return null; 505 | } 506 | 507 | /** 508 | * Determines where a function was defined within the source code. 509 | * @param {(Function|string)} func A function reference or serialized 510 | * function definition. 511 | * @return {?Object.} An object containing 512 | * the url, line, and column number of the defined function. 513 | */ 514 | function findSourceByFunctionBody(func) { 515 | var urls = [window.location.href], 516 | scripts = document.getElementsByTagName('script'), 517 | body, 518 | code = '' + func, 519 | codeRE = /^function(?:\s+([\w$]+))?\s*\(([\w\s,]*)\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, 520 | eventRE = /^function on([\w$]+)\s*\(event\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, 521 | re, 522 | parts, 523 | result; 524 | 525 | for (var i = 0; i < scripts.length; ++i) { 526 | var script = scripts[i]; 527 | if (script.src) { 528 | urls.push(script.src); 529 | } 530 | } 531 | 532 | if (!(parts = codeRE.exec(code))) { 533 | re = new RegExp(escapeRegExp(code).replace(/\s+/g, '\\s+')); 534 | } 535 | 536 | // not sure if this is really necessary, but I don’t have a test 537 | // corpus large enough to confirm that and it was in the original. 538 | else { 539 | var name = parts[1] ? '\\s+' + parts[1] : '', 540 | args = parts[2].split(',').join('\\s*,\\s*'); 541 | 542 | body = escapeRegExp(parts[3]).replace(/;$/, ';?'); // semicolon is inserted if the function ends with a comment.replace(/\s+/g, '\\s+'); 543 | re = new RegExp('function' + name + '\\s*\\(\\s*' + args + '\\s*\\)\\s*{\\s*' + body + '\\s*}'); 544 | } 545 | 546 | // look for a normal function definition 547 | if ((result = findSourceInUrls(re, urls))) { 548 | return result; 549 | } 550 | 551 | // look for an old-school event handler function 552 | if ((parts = eventRE.exec(code))) { 553 | var event = parts[1]; 554 | body = escapeCodeAsRegExpForMatchingInsideHTML(parts[2]); 555 | 556 | // look for a function defined in HTML as an onXXX handler 557 | re = new RegExp('on' + event + '=[\\\'"]\\s*' + body + '\\s*[\\\'"]', 'i'); 558 | 559 | if ((result = findSourceInUrls(re, urls[0]))) { 560 | return result; 561 | } 562 | 563 | // look for ??? 564 | re = new RegExp(body); 565 | 566 | if ((result = findSourceInUrls(re, urls))) { 567 | return result; 568 | } 569 | } 570 | 571 | return null; 572 | } 573 | 574 | // Contents of Exception in various browsers. 575 | // 576 | // SAFARI: 577 | // ex.message = Can't find variable: qq 578 | // ex.line = 59 579 | // ex.sourceId = 580238192 580 | // ex.sourceURL = http://... 581 | // ex.expressionBeginOffset = 96 582 | // ex.expressionCaretOffset = 98 583 | // ex.expressionEndOffset = 98 584 | // ex.name = ReferenceError 585 | // 586 | // FIREFOX: 587 | // ex.message = qq is not defined 588 | // ex.fileName = http://... 589 | // ex.lineNumber = 59 590 | // ex.stack = ...stack trace... (see the example below) 591 | // ex.name = ReferenceError 592 | // 593 | // CHROME: 594 | // ex.message = qq is not defined 595 | // ex.name = ReferenceError 596 | // ex.type = not_defined 597 | // ex.arguments = ['aa'] 598 | // ex.stack = ...stack trace... 599 | // 600 | // INTERNET EXPLORER: 601 | // ex.message = ... 602 | // ex.name = ReferenceError 603 | // 604 | // OPERA: 605 | // ex.message = ...message... (see the example below) 606 | // ex.name = ReferenceError 607 | // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message) 608 | // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace' 609 | 610 | /** 611 | * Computes stack trace information from the stack property. 612 | * Chrome and Gecko use this property. 613 | * @param {Error} ex 614 | * @return {?Object.} Stack trace information. 615 | */ 616 | function computeStackTraceFromStackProp(ex) { 617 | if (!ex.stack) { 618 | return null; 619 | } 620 | 621 | var chrome = /^\s*at (?:((?:\[object object\])?\S+(?: \[as \S+\])?) )?\(?((?:file|http|https):.*?):(\d+)(?::(\d+))?\)?\s*$/i, 622 | gecko = /^\s*(\S*)(?:\((.*?)\))?@((?:file|http|https).*?):(\d+)(?::(\d+))?\s*$/i, 623 | lines = ex.stack.split('\n'), 624 | stack = [], 625 | parts, 626 | element, 627 | reference = /^(.*) is undefined$/.exec(ex.message); 628 | 629 | for (var i = 0, j = lines.length; i < j; ++i) { 630 | if ((parts = gecko.exec(lines[i]))) { 631 | element = { 632 | 'url': parts[3], 633 | 'func': parts[1] || UNKNOWN_FUNCTION, 634 | 'args': parts[2] ? parts[2].split(',') : '', 635 | 'line': +parts[4], 636 | 'column': parts[5] ? +parts[5] : null 637 | }; 638 | } else if ((parts = chrome.exec(lines[i]))) { 639 | element = { 640 | 'url': parts[2], 641 | 'func': parts[1] || UNKNOWN_FUNCTION, 642 | 'line': +parts[3], 643 | 'column': parts[4] ? +parts[4] : null 644 | }; 645 | } else { 646 | continue; 647 | } 648 | 649 | if (!element.func && element.line) { 650 | element.func = guessFunctionName(element.url, element.line); 651 | } 652 | 653 | if (element.line) { 654 | element.context = gatherContext(element.url, element.line); 655 | } 656 | 657 | stack.push(element); 658 | } 659 | 660 | if (stack[0] && stack[0].line && !stack[0].column && reference) { 661 | stack[0].column = findSourceInLine(reference[1], stack[0].url, stack[0].line); 662 | } 663 | 664 | if (!stack.length) { 665 | return null; 666 | } 667 | 668 | return { 669 | 'mode': 'stack', 670 | 'name': ex.name, 671 | 'message': ex.message, 672 | 'url': document.location.href, 673 | 'stack': stack, 674 | 'useragent': navigator.userAgent 675 | }; 676 | } 677 | 678 | /** 679 | * Computes stack trace information from the stacktrace property. 680 | * Opera 10 uses this property. 681 | * @param {Error} ex 682 | * @return {?Object.} Stack trace information. 683 | */ 684 | function computeStackTraceFromStacktraceProp(ex) { 685 | // Access and store the stacktrace property before doing ANYTHING 686 | // else to it because Opera is not very good at providing it 687 | // reliably in other circumstances. 688 | var stacktrace = ex.stacktrace; 689 | 690 | var testRE = / line (\d+), column (\d+) in (?:]+)>|([^\)]+))\((.*)\) in (.*):\s*$/i, 691 | lines = stacktrace.split('\n'), 692 | stack = [], 693 | parts; 694 | 695 | for (var i = 0, j = lines.length; i < j; i += 2) { 696 | if ((parts = testRE.exec(lines[i]))) { 697 | var element = { 698 | 'line': +parts[1], 699 | 'column': +parts[2], 700 | 'func': parts[3] || parts[4], 701 | 'args': parts[5] ? parts[5].split(',') : [], 702 | 'url': parts[6] 703 | }; 704 | 705 | if (!element.func && element.line) { 706 | element.func = guessFunctionName(element.url, element.line); 707 | } 708 | if (element.line) { 709 | try { 710 | element.context = gatherContext(element.url, element.line); 711 | } catch (exc) {} 712 | } 713 | 714 | if (!element.context) { 715 | element.context = [lines[i + 1]]; 716 | } 717 | 718 | stack.push(element); 719 | } 720 | } 721 | 722 | if (!stack.length) { 723 | return null; 724 | } 725 | 726 | return { 727 | 'mode': 'stacktrace', 728 | 'name': ex.name, 729 | 'message': ex.message, 730 | 'url': document.location.href, 731 | 'stack': stack, 732 | 'useragent': navigator.userAgent 733 | }; 734 | } 735 | 736 | /** 737 | * NOT TESTED. 738 | * Computes stack trace information from an error message that includes 739 | * the stack trace. 740 | * Opera 9 and earlier use this method if the option to show stack 741 | * traces is turned on in opera:config. 742 | * @param {Error} ex 743 | * @return {?Object.} Stack information. 744 | */ 745 | function computeStackTraceFromOperaMultiLineMessage(ex) { 746 | // Opera includes a stack trace into the exception message. An example is: 747 | // 748 | // Statement on line 3: Undefined variable: undefinedFunc 749 | // Backtrace: 750 | // Line 3 of linked script file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.js: In function zzz 751 | // undefinedFunc(a); 752 | // Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function yyy 753 | // zzz(x, y, z); 754 | // Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function xxx 755 | // yyy(a, a, a); 756 | // Line 1 of function script 757 | // try { xxx('hi'); return false; } catch(ex) { TraceKit.report(ex); } 758 | // ... 759 | 760 | var lines = ex.message.split('\n'); 761 | if (lines.length < 4) { 762 | return null; 763 | } 764 | 765 | var lineRE1 = /^\s*Line (\d+) of linked script ((?:file|http|https)\S+)(?:: in function (\S+))?\s*$/i, 766 | lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|http|https)\S+)(?:: in function (\S+))?\s*$/i, 767 | lineRE3 = /^\s*Line (\d+) of function script\s*$/i, 768 | stack = [], 769 | scripts = document.getElementsByTagName('script'), 770 | inlineScriptBlocks = [], 771 | parts, 772 | i, 773 | len, 774 | source; 775 | 776 | for (i in scripts) { 777 | if (_has(scripts, i) && !scripts[i].src) { 778 | inlineScriptBlocks.push(scripts[i]); 779 | } 780 | } 781 | 782 | for (i = 2, len = lines.length; i < len; i += 2) { 783 | var item = null; 784 | if ((parts = lineRE1.exec(lines[i]))) { 785 | item = { 786 | 'url': parts[2], 787 | 'func': parts[3], 788 | 'line': +parts[1] 789 | }; 790 | } else if ((parts = lineRE2.exec(lines[i]))) { 791 | item = { 792 | 'url': parts[3], 793 | 'func': parts[4] 794 | }; 795 | var relativeLine = (+parts[1]); // relative to the start of the