diff --git a/.jshintrc b/.jshintrc index 6a123502afdb..79444bf25963 100644 --- a/.jshintrc +++ b/.jshintrc @@ -3,8 +3,8 @@ "globalstrict": true, "browser": true, "predef": [ - "TraceKit", "console", - "_slice" + "module", + "require" ] } diff --git a/Gruntfile.js b/Gruntfile.js index 54c18d450e4b..c90a75eabb55 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,15 +1,11 @@ +var proxyquire = require('proxyquireify'); + module.exports = function(grunt) { "use strict"; var _ = require('lodash'); var path = require('path'); - - var coreFiles = [ - 'template/_header.js', - 'vendor/**/*.js', - 'src/**/*.js', - 'template/_footer.js' - ]; + var through = require('through2'); var excludedPlugins = [ 'react-native' @@ -26,6 +22,21 @@ module.exports = function(grunt) { return path; }); + // custom browserify transformer to re-write plugins to + // self-register with Raven via addPlugin + function AddPluginBrowserifyTransformer() { + return function (file) { + return through(function (buf, enc, next) { + buf = buf.toString('utf8'); + if (/plugins/.test(file)) { + buf += "\nrequire('../src/singleton').addPlugin(module.exports);"; + } + this.push(buf); + next(); + }); + }; + } + // Taken from http://dzone.com/snippets/calculate-all-combinations var combine = function (a) { var fn = function (n, src, got, all) { @@ -65,7 +76,7 @@ module.exports = function(grunt) { key.sort(); var dest = path.join('build/', key.join(','), '/raven.js'); - dict[dest] = coreFiles.concat(comb); + dict[dest] = ['src/singleton.js'].concat(comb); return dict; }, {}); @@ -75,18 +86,35 @@ module.exports = function(grunt) { aws: grunt.file.exists('aws.json') ? grunt.file.readJSON('aws.json'): {}, clean: ['build'], - concat: { + + browserify: { options: { - separator: '\n', - banner: grunt.file.read('template/_copyright.js'), - process: true + browserifyOptions: { + banner: grunt.file.read('template/_copyright.js'), + standalone: 'Raven' // umd + } }, core: { - src: coreFiles.concat(plugins), + src: 'src/singleton.js', dest: 'build/raven.js' }, - all: { - files: pluginConcatFiles + plugins: { + files: pluginConcatFiles, + options: { + transform: [ + [ new AddPluginBrowserifyTransformer() ] + ] + } + }, + test: { + src: 'test/**/*.test.js', + dest: 'build/raven.test.js', + options: { + browserifyOptions: { + debug: true // source maps + }, + plugin: [proxyquire.plugin] + } } }, @@ -100,7 +128,13 @@ module.exports = function(grunt) { sourceMappingURL: function (dest) { return path.basename(dest, '.js') + '.map'; }, - preserveComments: 'some' + preserveComments: 'some', + compress: { + dead_code: true, + global_defs: { + "TEST": false + } + } }, dist: { src: ['build/**/*.js'], @@ -251,13 +285,13 @@ module.exports = function(grunt) { // Grunt contrib tasks grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-contrib-concat'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-connect'); grunt.loadNpmTasks('grunt-contrib-copy'); // 3rd party Grunt tasks + grunt.loadNpmTasks('grunt-browserify'); grunt.loadNpmTasks('grunt-mocha'); grunt.loadNpmTasks('grunt-release'); grunt.loadNpmTasks('grunt-s3'); @@ -266,15 +300,16 @@ module.exports = function(grunt) { // Build tasks grunt.registerTask('_prep', ['clean', 'gitinfo', 'version']); - grunt.registerTask('concat.core', ['_prep', 'concat:core']); - grunt.registerTask('concat.all', ['_prep', 'concat:all']); - grunt.registerTask('build.core', ['concat.core', 'uglify', 'fixSourceMaps', 'sri:dist']); - grunt.registerTask('build.all', ['concat.all', 'uglify', 'fixSourceMaps', 'sri:dist', 'sri:build']); + grunt.registerTask('browserify.core', ['_prep', 'browserify:core']); + grunt.registerTask('browserify.plugins', ['_prep', 'browserify:plugins']); + grunt.registerTask('build.test', ['_prep', 'browserify:test']); + grunt.registerTask('build.core', ['browserify.core', 'uglify', 'fixSourceMaps', 'sri:dist']); + grunt.registerTask('build.all', ['browserify.plugins', 'uglify', 'fixSourceMaps', 'sri:dist', 'sri:build']); grunt.registerTask('build', ['build.all']); grunt.registerTask('dist', ['build.core', 'copy:dist']); // Test task - grunt.registerTask('test', ['jshint', 'mocha']); + grunt.registerTask('test', ['jshint', 'browserify.core', 'browserify:test', 'mocha']); // Webserver tasks grunt.registerTask('run:test', ['connect:test']); diff --git a/package.json b/package.json index 7fa452898888..fb9e4976fb6e 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,11 @@ "type": "git", "url": "git://github.com/getsentry/raven-js.git" }, - "main": "dist/raven.js", + "main": "src/singleton.js", "devDependencies": { "chai": "~1.8.1", "grunt": "^0.4.5", + "grunt-browserify": "^4.0.1", "grunt-cli": "~0.1.9", "grunt-contrib-clean": "~0.4.0", "grunt-contrib-concat": "~0.3.0", @@ -29,7 +30,9 @@ "grunt-sri": "mattrobenolt/grunt-sri#pretty", "jquery": "^2.1.4", "lodash": "~2.4.0", - "sinon": "~1.7.3" + "proxyquireify": "^3.0.0", + "sinon": "~1.7.3", + "through2": "^2.0.0" }, "keywords": [ "exceptions", diff --git a/plugins/angular.js b/plugins/angular.js index dd929f5cddeb..4b4cff987554 100644 --- a/plugins/angular.js +++ b/plugins/angular.js @@ -3,60 +3,58 @@ * * Provides an $exceptionHandler for Angular.js */ -;(function(window) { 'use strict'; -var angular = window.angular, - Raven = window.Raven; +// See https://github.com/angular/angular.js/blob/v1.4.7/src/minErr.js +var angularPattern = /^\[((?:[$a-zA-Z0-9]+:)?(?:[$a-zA-Z0-9]+))\] (.+?)\n(\S+)$/; -// quit if angular isn't on the page -if (!(angular && Raven)) return; +function angularPlugin(Raven, angular) { + /*jshint validthis:true*/ + angular = angular || window.angular; -function RavenProvider() { - this.$get = ['$window', function($window, $log) { - return $window.Raven; - }]; -} + if (!angular) return; -function ExceptionHandlerProvider($provide) { - $provide.decorator('$exceptionHandler', - ['Raven', '$delegate', exceptionHandler]); -} + function RavenProvider() { + this.$get = ['$window', function($window) { + return Raven; + }]; + } -function exceptionHandler(Raven, $delegate) { - return function (ex, cause) { - Raven.captureException(ex, { - extra: { cause: cause } - }); - $delegate(ex, cause); - }; -} + function ExceptionHandlerProvider($provide) { + $provide.decorator('$exceptionHandler', + ['Raven', '$delegate', exceptionHandler]); + } -// See https://github.com/angular/angular.js/blob/v1.4.7/src/minErr.js -var angularPattern = /^\[((?:[$a-zA-Z0-9]+:)?(?:[$a-zA-Z0-9]+))\] (.+?)\n(\S+)$/; + function exceptionHandler(Raven, $delegate) { + return function (ex, cause) { + Raven.captureException(ex, { + extra: { cause: cause } + }); + $delegate(ex, cause); + }; + } -Raven.addPlugin(function () { angular.module('ngRaven', []) .provider('Raven', RavenProvider) .config(['$provide', ExceptionHandlerProvider]); -}); - -Raven.setDataCallback(function(data) { - // We only care about mutating an exception - var exception = data.exception; - if (exception) { - exception = exception.values[0]; - var matches = angularPattern.exec(exception.value); - - if (matches) { - // This type now becomes something like: $rootScope:inprog - exception.type = matches[1]; - exception.value = matches[2]; - data.message = exception.type + ': ' + exception.value; - // auto set a new tag specifically for the angular error url - data.extra.angularDocs = matches[3].substr(0, 250); + + Raven.setDataCallback(function(data) { + // We only care about mutating an exception + var exception = data.exception; + if (exception) { + exception = exception.values[0]; + var matches = angularPattern.exec(exception.value); + + if (matches) { + // This type now becomes something like: $rootScope:inprog + exception.type = matches[1]; + exception.value = matches[2]; + data.message = exception.type + ': ' + exception.value; + // auto set a new tag specifically for the angular error url + data.extra.angularDocs = matches[3].substr(0, 250); + } } - } -}); + }); +} -}(typeof window !== 'undefined' ? window : this)); +module.exports = angularPlugin; diff --git a/plugins/backbone.js b/plugins/backbone.js index 6e6b64c2b034..6c777688b1f5 100644 --- a/plugins/backbone.js +++ b/plugins/backbone.js @@ -3,57 +3,54 @@ * * Patches Backbone.Events callbacks. */ -;(function(window) { 'use strict'; -if (window.Raven) Raven.addPlugin(function backbonePlugin() { +function backbonePlugin(Raven, Backbone) { + Backbone = Backbone || window.Backbone; -var Backbone = window.Backbone; + // quit if Backbone isn't on the page + if (!Backbone) return; -// quit if Backbone isn't on the page -if (!Backbone) return; - -function makeBackboneEventsOn(oldOn) { - return function BackboneEventsOn(name, callback, context) { - var wrapCallback = function (cb) { - if (Object.prototype.toString.call(cb) === '[object Function]') { - var _callback = cb._callback || cb; - cb = Raven.wrap(cb); - cb._callback = _callback; - } - return cb; - }; - if (Object.prototype.toString.call(name) === '[object Object]') { - // Handle event maps. - for (var key in name) { - if (name.hasOwnProperty(key)) { - name[key] = wrapCallback(name[key]); + function makeBackboneEventsOn(oldOn) { + return function BackboneEventsOn(name, callback, context) { + var wrapCallback = function (cb) { + if (Object.prototype.toString.call(cb) === '[object Function]') { + var _callback = cb._callback || cb; + cb = Raven.wrap(cb); + cb._callback = _callback; } + return cb; + }; + if (Object.prototype.toString.call(name) === '[object Object]') { + // Handle event maps. + for (var key in name) { + if (name.hasOwnProperty(key)) { + name[key] = wrapCallback(name[key]); + } + } + } else { + callback = wrapCallback(callback); } - } else { - callback = wrapCallback(callback); - } - return oldOn.call(this, name, callback, context); - }; -} - -// We're too late to catch all of these by simply patching Backbone.Events.on -var affectedObjects = [ - Backbone.Events, - Backbone, - Backbone.Model.prototype, - Backbone.Collection.prototype, - Backbone.View.prototype, - Backbone.Router.prototype, - Backbone.History.prototype -], i = 0, l = affectedObjects.length; - -for (; i < l; i++) { - var affected = affectedObjects[i]; - affected.on = makeBackboneEventsOn(affected.on); - affected.bind = affected.on; + return oldOn.call(this, name, callback, context); + }; + } + + // We're too late to catch all of these by simply patching Backbone.Events.on + var affectedObjects = [ + Backbone.Events, + Backbone, + Backbone.Model.prototype, + Backbone.Collection.prototype, + Backbone.View.prototype, + Backbone.Router.prototype, + Backbone.History.prototype + ], i = 0, l = affectedObjects.length; + + for (; i < l; i++) { + var affected = affectedObjects[i]; + affected.on = makeBackboneEventsOn(affected.on); + affected.bind = affected.on; + } } -}); - -}(typeof window !== 'undefined' ? window : this)); +module.exports = backbonePlugin; diff --git a/plugins/console.js b/plugins/console.js index c3e098a72e4e..3f2b14a4966f 100644 --- a/plugins/console.js +++ b/plugins/console.js @@ -4,49 +4,41 @@ * Monkey patches console.* calls into Sentry messages with * their appropriate log levels. (Experimental) */ -;(function(window) { 'use strict'; -if (window.Raven) Raven.addPlugin(function ConsolePlugin() { - -var console = window.console || {}; - -var originalConsole = console, - logLevels = ['debug', 'info', 'warn', 'error'], - level = logLevels.pop(); - -var logForGivenLevel = function(level) { - var originalConsoleLevel = console[level]; - - // warning level is the only level that doesn't map up - // correctly with what Sentry expects. - if (level === 'warn') level = 'warning'; - return function () { - var args = [].slice.call(arguments); - Raven.captureMessage('' + args[0], {level: level, logger: 'console', extra: { 'arguments': args }}); - - // this fails for some browsers. :( - if (originalConsoleLevel) { - // IE9 doesn't allow calling apply on console functions directly - // See: https://stackoverflow.com/questions/5472938/does-ie9-support-console-log-and-is-it-a-real-function#answer-5473193 - Function.prototype.bind - .call(originalConsoleLevel, originalConsole) - .apply(originalConsole, args); - } +function consolePlugin(Raven, console) { + console = console || window.console || {}; + + var originalConsole = console, + logLevels = ['debug', 'info', 'warn', 'error'], + level = logLevels.pop(); + + var logForGivenLevel = function(level) { + var originalConsoleLevel = console[level]; + + // warning level is the only level that doesn't map up + // correctly with what Sentry expects. + if (level === 'warn') level = 'warning'; + return function () { + var args = [].slice.call(arguments); + Raven.captureMessage('' + args[0], {level: level, logger: 'console', extra: { 'arguments': args }}); + + // this fails for some browsers. :( + if (originalConsoleLevel) { + // IE9 doesn't allow calling apply on console functions directly + // See: https://stackoverflow.com/questions/5472938/does-ie9-support-console-log-and-is-it-a-real-function#answer-5473193 + Function.prototype.bind + .call(originalConsoleLevel, originalConsole) + .apply(originalConsole, args); + } + }; }; -}; -while(level) { - console[level] = logForGivenLevel(level); - level = logLevels.pop(); + while(level) { + console[level] = logForGivenLevel(level); + level = logLevels.pop(); + } } -// export -window.console = console; - -// End of plugin factory -}); - -// console would require `window`, so we don't allow it to be optional -}(window)); +module.exports = consolePlugin; diff --git a/plugins/ember.js b/plugins/ember.js index 3d669fad9d6a..fd58c590f262 100644 --- a/plugins/ember.js +++ b/plugins/ember.js @@ -3,32 +3,29 @@ * * Patches event handler callbacks and ajax callbacks. */ -;(function(window) { 'use strict'; -if (window.Raven) Raven.addPlugin(function EmberPlugin() { +function emberPlugin(Raven, Ember) { + /*jshint validthis:true*/ + Ember = Ember || window.Ember; -var Ember = window.Ember; + // quit if Ember isn't on the page + if (!Ember) return; -// quit if Ember isn't on the page -if (!Ember) return; + var _oldOnError = Ember.onerror; + Ember.onerror = function EmberOnError(error) { + Raven.captureException(error); + if (typeof _oldOnError === 'function') { + _oldOnError.call(this, error); + } + }; + Ember.RSVP.on('error', function (reason) { + if (reason instanceof Error) { + Raven.captureException(reason, {extra: {context: 'Unhandled Promise error detected'}}); + } else { + Raven.captureMessage('Unhandled Promise error detected', {extra: {reason: reason}}); + } + }); +} -var _oldOnError = Ember.onerror; -Ember.onerror = function EmberOnError(error) { - Raven.captureException(error); - if (typeof _oldOnError === 'function') { - _oldOnError.call(this, error); - } -}; -Ember.RSVP.on('error', function (reason) { - if (reason instanceof Error) { - Raven.captureException(reason, {extra: {context: 'Unhandled Promise error detected'}}); - } else { - Raven.captureMessage('Unhandled Promise error detected', {extra: {reason: reason}}); - } -}); - -// End of plugin factory -}); - -}(typeof window !== 'undefined' ? window : this)); +module.exports = emberPlugin; diff --git a/plugins/jquery.js b/plugins/jquery.js index 63203f5329c6..34fa47566d7c 100644 --- a/plugins/jquery.js +++ b/plugins/jquery.js @@ -3,106 +3,103 @@ * * Patches event handler callbacks and ajax callbacks. */ -;(function(window) { 'use strict'; -if (window.Raven) Raven.addPlugin(function jQueryPlugin() { - -var $ = window.jQuery; - -// quit if jQuery isn't on the page -if (!$) return; - -var _oldEventAdd = $.event.add; -$.event.add = function ravenEventAdd(elem, types, handler, data, selector) { - var _handler; - - if (handler && handler.handler) { - _handler = handler.handler; - handler.handler = Raven.wrap(handler.handler); - } else { - _handler = handler; - handler = Raven.wrap(handler); - } - - // If the handler we are attaching doesn’t have the same guid as - // the original, it will never be removed when someone tries to - // unbind the original function later. Technically as a result of - // this our guids are no longer globally unique, but whatever, that - // never hurt anybody RIGHT?! - if (_handler.guid) { - handler.guid = _handler.guid; - } else { - handler.guid = _handler.guid = $.guid++; - } - - return _oldEventAdd.call(this, elem, types, handler, data, selector); -}; - -var _oldReady = $.fn.ready; -$.fn.ready = function ravenjQueryReadyWrapper(fn) { - return _oldReady.call(this, Raven.wrap(fn)); -}; - -var _oldAjax = $.ajax; -$.ajax = function ravenAjaxWrapper(url, options) { - var keys = ['complete', 'error', 'success'], key; - - // Taken from https://github.com/jquery/jquery/blob/eee2eaf1d7a189d99106423a4206c224ebd5b848/src/ajax.js#L311-L318 - // If url is an object, simulate pre-1.5 signature - if (typeof url === 'object') { - options = url; - url = undefined; - } - - // Force options to be an object - options = options || {}; - - /*jshint -W084*/ - while (key = keys.pop()) { - if ($.isFunction(options[key])) { - options[key] = Raven.wrap(options[key]); +function jQueryPlugin(Raven, jQuery) { + /*jshint validthis:true*/ + var $ = jQuery || window.jQuery; + + // quit if jQuery isn't on the page + if (!$) return; + + var _oldEventAdd = $.event.add; + $.event.add = function ravenEventAdd(elem, types, handler, data, selector) { + var _handler; + + if (handler && handler.handler) { + _handler = handler.handler; + handler.handler = Raven.wrap(handler.handler); + } else { + _handler = handler; + handler = Raven.wrap(handler); } - } - /*jshint +W084*/ - - try { - var jqXHR = _oldAjax.call(this, url, options); - // jqXHR.complete is not a regular deferred callback - if ($.isFunction(jqXHR.complete)) - jqXHR.complete = Raven.wrap(jqXHR.complete); - return jqXHR; - } catch (e) { - Raven.captureException(e); - throw e; - } -}; - -var _oldDeferred = $.Deferred; -$.Deferred = function ravenDeferredWrapper(func) { - return !_oldDeferred ? null : _oldDeferred(function beforeStartWrapper(deferred) { - var methods = ['resolve', 'reject', 'notify', 'resolveWith', 'rejectWith', 'notifyWith'], method; - - // since jQuery 1.9, deferred[resolve | reject | notify] are calling internally - // deferred[resolveWith | rejectWith | notifyWith] but we need to wrap them as well - // to support all previous versions. + + // If the handler we are attaching doesn’t have the same guid as + // the original, it will never be removed when someone tries to + // unbind the original function later. Technically as a result of + // this our guids are no longer globally unique, but whatever, that + // never hurt anybody RIGHT?! + if (_handler.guid) { + handler.guid = _handler.guid; + } else { + handler.guid = _handler.guid = $.guid++; + } + + return _oldEventAdd.call(this, elem, types, handler, data, selector); + }; + + var _oldReady = $.fn.ready; + $.fn.ready = function ravenjQueryReadyWrapper(fn) { + return _oldReady.call(this, Raven.wrap(fn)); + }; + + var _oldAjax = $.ajax; + $.ajax = function ravenAjaxWrapper(url, options) { + var keys = ['complete', 'error', 'success'], key; + + // Taken from https://github.com/jquery/jquery/blob/eee2eaf1d7a189d99106423a4206c224ebd5b848/src/ajax.js#L311-L318 + // If url is an object, simulate pre-1.5 signature + if (typeof url === 'object') { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; /*jshint -W084*/ - while (method = methods.pop()) { - if ($.isFunction(deferred[method])) { - deferred[method] = Raven.wrap(deferred[method]); + while (key = keys.pop()) { + if ($.isFunction(options[key])) { + options[key] = Raven.wrap(options[key]); } } /*jshint +W084*/ - // Call given func if any - if (func) { - func.call(deferred, deferred); + try { + var jqXHR = _oldAjax.call(this, url, options); + // jqXHR.complete is not a regular deferred callback + if ($.isFunction(jqXHR.complete)) + jqXHR.complete = Raven.wrap(jqXHR.complete); + return jqXHR; + } catch (e) { + Raven.captureException(e); + throw e; } - }); -}; + }; + + var _oldDeferred = $.Deferred; + $.Deferred = function ravenDeferredWrapper(func) { + return !_oldDeferred ? null : _oldDeferred(function beforeStartWrapper(deferred) { + var methods = ['resolve', 'reject', 'notify', 'resolveWith', 'rejectWith', 'notifyWith'], method; + + // since jQuery 1.9, deferred[resolve | reject | notify] are calling internally + // deferred[resolveWith | rejectWith | notifyWith] but we need to wrap them as well + // to support all previous versions. + + /*jshint -W084*/ + while (method = methods.pop()) { + if ($.isFunction(deferred[method])) { + deferred[method] = Raven.wrap(deferred[method]); + } + } + /*jshint +W084*/ -// End of plugin factory -}); + // Call given func if any + if (func) { + func.call(deferred, deferred); + } + }); + }; +} -}(typeof window !== 'undefined' ? window : this)); +module.exports = jQueryPlugin; diff --git a/plugins/native.js b/plugins/native.js index bf8b7e6d9b83..d78cb505d12f 100644 --- a/plugins/native.js +++ b/plugins/native.js @@ -4,35 +4,31 @@ * Extends support for global error handling for asynchronous browser * functions. Adopted from Closure Library's errorhandler.js. */ -;(function(window) { 'use strict'; -if (window.Raven) Raven.addPlugin(function nativePlugin() { - -var _helper = function _helper(fnName) { - var originalFn = window[fnName]; - window[fnName] = function ravenAsyncExtension() { - // Make a copy of the arguments - var args = [].slice.call(arguments); - var originalCallback = args[0]; - if (typeof (originalCallback) === 'function') { - args[0] = Raven.wrap(originalCallback); - } - // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it - // also supports only two arguments and doesn't care what this is, so we - // can just call the original function directly. - if (originalFn.apply) { - return originalFn.apply(this, args); - } else { - return originalFn(args[0], args[1]); - } +function nativePlugin(Raven) { + var _helper = function _helper(fnName) { + var originalFn = window[fnName]; + window[fnName] = function ravenAsyncExtension() { + // Make a copy of the arguments + var args = [].slice.call(arguments); + var originalCallback = args[0]; + if (typeof (originalCallback) === 'function') { + args[0] = Raven.wrap(originalCallback); + } + // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it + // also supports only two arguments and doesn't care what this is, so we + // can just call the original function directly. + if (originalFn.apply) { + return originalFn.apply(this, args); + } else { + return originalFn(args[0], args[1]); + } + }; }; -}; - -_helper('setTimeout'); -_helper('setInterval'); -// End of plugin factory -}); + _helper('setTimeout'); + _helper('setInterval'); +} -}(typeof window !== 'undefined' ? window : this)); +module.exports = nativePlugin; diff --git a/plugins/react-native.js b/plugins/react-native.js index 80ff300c9eea..1ce887f97eb3 100644 --- a/plugins/react-native.js +++ b/plugins/react-native.js @@ -7,19 +7,16 @@ * var Raven = require('raven-js'); * require('raven-js/plugins/react-native')(Raven); */ +'use strict'; var DEVICE_PATH_RE = /^\/var\/mobile\/Containers\/Bundle\/Application\/[^\/]+\/[^\.]+\.app/; function normalizeUrl(url) { - "use strict"; - return url .replace(/^file\:\/\//, '') .replace(DEVICE_PATH_RE, ''); } -module.exports = function (Raven) { - "use strict"; - +function reactNativePlugin(Raven) { function urlencode(obj) { var pairs = []; for (var key in obj) { @@ -73,4 +70,6 @@ module.exports = function (Raven) { }); ErrorUtils.setGlobalHandler(Raven.captureException); -}; +} + +module.exports = reactNativePlugin; diff --git a/plugins/require.js b/plugins/require.js index 768a935b2ff5..71eddb284ba2 100644 --- a/plugins/require.js +++ b/plugins/require.js @@ -1,19 +1,16 @@ +/*global define*/ /** * require.js plugin * * Automatically wrap define/require callbacks. (Experimental) */ -;(function(window) { 'use strict'; -if (window.Raven) Raven.addPlugin(function RequirePlugin() { - -if (typeof define === 'function' && define.amd) { - window.define = Raven.wrap({deep: false}, define); - window.require = Raven.wrap({deep: false}, require); +function requirePlugin(Raven) { + if (typeof define === 'function' && define.amd) { + window.define = Raven.wrap({deep: false}, define); + window.require = Raven.wrap({deep: false}, require); + } } -// End of plugin factory -}); - -}(window)); +module.exports = requirePlugin; diff --git a/src/configError.js b/src/configError.js new file mode 100644 index 000000000000..bb48808f0b77 --- /dev/null +++ b/src/configError.js @@ -0,0 +1,10 @@ +'use strict'; + +function RavenConfigError(message) { + this.name = 'RavenConfigError'; + this.message = message; +} +RavenConfigError.prototype = new Error(); +RavenConfigError.prototype.constructor = RavenConfigError; + +module.exports = RavenConfigError; diff --git a/src/raven.js b/src/raven.js index 0f1d5d21f2be..d3844c5ee441 100644 --- a/src/raven.js +++ b/src/raven.js @@ -1,20 +1,43 @@ /*global XDomainRequest:false*/ 'use strict'; +var TraceKit = require('../vendor/TraceKit/tracekit'); +var RavenConfigError = require('./configError'); +var utils = require('./utils'); + +var isFunction = utils.isFunction; +var isUndefined = utils.isUndefined; +var isError = utils.isError; +var isEmptyObject = utils.isEmptyObject; +var hasKey = utils.hasKey; +var joinRegExp = utils.joinRegExp; +var each = utils.each; +var objectMerge = utils.objectMerge; +var truncate = utils.truncate; +var urlencode = utils.urlencode; +var uuid4 = utils.uuid4; + +var dsnKeys = 'source protocol user pass host port path'.split(' '), + dsnPattern = /^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/; + +function now() { + return +new Date(); +} + // First, check for JSON support // If there is no JSON, we no-op the core features of Raven // since JSON is required to encode the payload -var _Raven = window.Raven, - hasJSON = !!(typeof JSON === 'object' && JSON.stringify), +function Raven() { + this._hasJSON = !!(typeof JSON === 'object' && JSON.stringify); // Raven can run in contexts where there's no document (react-native) - hasDocument = typeof document !== 'undefined', - lastCapturedException, - lastEventId, - globalServer, - globalKey, - globalProject, - globalContext = {}, - globalOptions = { + this._hasDocument = typeof document !== 'undefined'; + this._lastCapturedException = null; + this._lastEventId = null; + this._globalServer = null; + this._globalKey = null; + this._globalProject = null; + this._globalContext = {}; + this._globalOptions = { logger: 'javascript', ignoreErrors: [], ignoreUrls: [], @@ -23,39 +46,30 @@ var _Raven = window.Raven, crossOrigin: 'anonymous', collectWindowErrors: true, maxMessageLength: 100 - }, - isRavenInstalled = false, - objectPrototype = Object.prototype, + }; + this._isRavenInstalled = false; // capture references to window.console *and* all its methods first // before the console plugin has a chance to monkey patch - originalConsole = window.console || {}, - originalConsoleMethods = {}, - plugins = [], - startTime = now(); + this._originalConsole = window.console || {}; + this._originalConsoleMethods = {}; + this._plugins = []; + this._startTime = now(); -for (var method in originalConsole) { - originalConsoleMethods[method] = originalConsole[method]; + for (var method in this._originalConsole) { + this._originalConsoleMethods[method] = this._originalConsole[method]; + } } + /* * The core Raven singleton * * @this {Raven} */ -var Raven = { - VERSION: '<%= pkg.version %>', - debug: false, +Raven.prototype = { + VERSION: '<%= pkg.version %>', - /* - * Allow multiple versions of Raven to be installed. - * Strip Raven from the global context and returns the instance. - * - * @return {Raven} - */ - noConflict: function() { - window.Raven = _Raven; - return Raven; - }, + debug: true, /* * Configure Raven with a DSN and extra options @@ -65,13 +79,15 @@ var Raven = { * @return {Raven} */ config: function(dsn, options) { - if (globalServer) { - logDebug('error', 'Error: Raven has already been configured'); - return Raven; + var self = this; + + if (this._globalServer) { + this._logDebug('error', 'Error: Raven has already been configured'); + return this; } - if (!dsn) return Raven; + if (!dsn) return this; - var uri = parseDSN(dsn), + var uri = this._parseDSN(dsn), lastSlash = uri.path.lastIndexOf('/'), path = uri.path.substr(1, lastSlash); @@ -80,50 +96,48 @@ var Raven = { each(options, function(key, value){ // tags and extra are special and need to be put into context if (key == 'tags' || key == 'extra') { - globalContext[key] = value; + self._globalContext[key] = value; } else { - globalOptions[key] = value; + self._globalOptions[key] = value; } }); } // "Script error." is hard coded into browsers for errors that it can't read. // this is the result of a script being pulled in from an external domain and CORS. - globalOptions.ignoreErrors.push(/^Script error\.?$/); - globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/); + this._globalOptions.ignoreErrors.push(/^Script error\.?$/); + this._globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/); // join regexp rules into one big rule - globalOptions.ignoreErrors = joinRegExp(globalOptions.ignoreErrors); - globalOptions.ignoreUrls = globalOptions.ignoreUrls.length ? joinRegExp(globalOptions.ignoreUrls) : false; - globalOptions.whitelistUrls = globalOptions.whitelistUrls.length ? joinRegExp(globalOptions.whitelistUrls) : false; - globalOptions.includePaths = joinRegExp(globalOptions.includePaths); + this._globalOptions.ignoreErrors = joinRegExp(this._globalOptions.ignoreErrors); + this._globalOptions.ignoreUrls = this._globalOptions.ignoreUrls.length ? joinRegExp(this._globalOptions.ignoreUrls) : false; + this._globalOptions.whitelistUrls = this._globalOptions.whitelistUrls.length ? joinRegExp(this._globalOptions.whitelistUrls) : false; + this._globalOptions.includePaths = joinRegExp(this._globalOptions.includePaths); - globalKey = uri.user; - globalProject = uri.path.substr(lastSlash + 1); + this._globalKey = uri.user; + this._globalProject = uri.path.substr(lastSlash + 1); // assemble the endpoint from the uri pieces - globalServer = '//' + uri.host + + this._globalServer = '//' + uri.host + (uri.port ? ':' + uri.port : '') + - '/' + path + 'api/' + globalProject + '/store/'; + '/' + path + 'api/' + this._globalProject + '/store/'; - // can safely use protocol relative (//) if target host is - // app.getsentry.com; otherwise use protocol from DSN if (uri.protocol && uri.host !== 'app.getsentry.com') { - globalServer = uri.protocol + ':' + globalServer; + this._globalServer = uri.protocol + ':' + this._globalServer; } - if (globalOptions.fetchContext) { + if (this._globalOptions.fetchContext) { TraceKit.remoteFetching = true; } - if (globalOptions.linesOfContext) { - TraceKit.linesOfContext = globalOptions.linesOfContext; + if (this._globalOptions.linesOfContext) { + TraceKit.linesOfContext = this._globalOptions.linesOfContext; } - TraceKit.collectWindowErrors = !!globalOptions.collectWindowErrors; + TraceKit.collectWindowErrors = !!this._globalOptions.collectWindowErrors; // return for chaining - return Raven; + return this; }, /* @@ -135,18 +149,20 @@ var Raven = { * @return {Raven} */ install: function() { - if (isSetup() && !isRavenInstalled) { - TraceKit.report.subscribe(handleStackInfo); + var self = this; + if (this.isSetup() && !this._isRavenInstalled) { + TraceKit.report.subscribe(function () { + // maintain 'self' + self._handleStackInfo.apply(self, arguments); + }); // Install all of the plugins - each(plugins, function(_, plugin) { - plugin(); - }); + this._drainPlugins(); - isRavenInstalled = true; + this._isRavenInstalled = true; } - return Raven; + return this; }, /* @@ -164,7 +180,7 @@ var Raven = { options = undefined; } - return Raven.wrap(options, func).apply(this, args); + return this.wrap(options, func).apply(this, args); }, /* @@ -175,6 +191,8 @@ var Raven = { * @return {function} The newly wrapped functions with a context */ wrap: function(options, func) { + var self = this; + // 1 argument has been passed, and it's not a function // so just return it if (isUndefined(func) && !isFunction(options)) { @@ -204,13 +222,13 @@ var Raven = { // Recursively wrap all of a function's arguments that are // functions themselves. - while(i--) args[i] = deep ? Raven.wrap(options, arguments[i]) : arguments[i]; + while(i--) args[i] = deep ? self.wrap(options, arguments[i]) : arguments[i]; try { /*jshint -W040*/ return func.apply(this, args); } catch(e) { - Raven.captureException(e, options); + self.captureException(e, options); throw e; } } @@ -238,9 +256,9 @@ var Raven = { */ uninstall: function() { TraceKit.report.uninstall(); - isRavenInstalled = false; + this._isRavenInstalled = false; - return Raven; + return this; }, /* @@ -252,10 +270,10 @@ var Raven = { */ captureException: function(ex, options) { // If not an Error is passed through, recall as a message instead - if (!isError(ex)) return Raven.captureMessage(ex, options); + if (!isError(ex)) return this.captureMessage(ex, options); // Store the raw exception object for potential debugging and introspection - lastCapturedException = ex; + this._lastCapturedException = ex; // TraceKit.report will re-raise any exception passed to it, // which means you have to wrap it in try/catch. Instead, we @@ -264,14 +282,14 @@ var Raven = { // report on. try { var stack = TraceKit.computeStackTrace(ex); - handleStackInfo(stack, options); + this._handleStackInfo(stack, options); } catch(ex1) { if(ex !== ex1) { throw ex1; } } - return Raven; + return this; }, /* @@ -285,24 +303,29 @@ var Raven = { // config() automagically converts ignoreErrors from a list to a RegExp so we need to test for an // early call; we'll error on the side of logging anything called before configuration since it's // probably something you should see: - if (!!globalOptions.ignoreErrors.test && globalOptions.ignoreErrors.test(msg)) { + if (!!this._globalOptions.ignoreErrors.test && this._globalOptions.ignoreErrors.test(msg)) { return; } // Fire away! - send( + this._send( objectMerge({ message: msg + '' // Make sure it's actually a string }, options) ); - return Raven; + return this; }, - addPlugin: function(plugin) { - plugins.push(plugin); - if (isRavenInstalled) plugin(); - return Raven; + addPlugin: function(plugin /*arg1, arg2, ... argN*/) { + var pluginArgs = Array.prototype.slice.call(arguments, 1); + + this._plugins.push([plugin, pluginArgs]); + if (this._isRavenInstalled) { + this._drainPlugins(); + } + + return this; }, /* @@ -313,9 +336,9 @@ var Raven = { */ setUserContext: function(user) { // Intentionally do not merge here since that's an unexpected behavior. - globalContext.user = user; + this._globalContext.user = user; - return Raven; + return this; }, /* @@ -325,9 +348,9 @@ var Raven = { * @return {Raven} */ setExtraContext: function(extra) { - mergeContext('extra', extra); + this._mergeContext('extra', extra); - return Raven; + return this; }, /* @@ -337,9 +360,9 @@ var Raven = { * @return {Raven} */ setTagsContext: function(tags) { - mergeContext('tags', tags); + this._mergeContext('tags', tags); - return Raven; + return this; }, /* @@ -348,9 +371,9 @@ var Raven = { * @return {Raven} */ clearContext: function() { - globalContext = {}; + this._globalContext = {}; - return Raven; + return this; }, /* @@ -360,7 +383,7 @@ var Raven = { */ getContext: function() { // lol javascript - return JSON.parse(JSON.stringify(globalContext)); + return JSON.parse(JSON.stringify(this._globalContext)); }, /* @@ -370,9 +393,9 @@ var Raven = { * @return {Raven} */ setRelease: function(release) { - globalOptions.release = release; + this._globalOptions.release = release; - return Raven; + return this; }, /* @@ -383,9 +406,9 @@ var Raven = { * @return {Raven} */ setDataCallback: function(callback) { - globalOptions.dataCallback = callback; + this._globalOptions.dataCallback = callback; - return Raven; + return this; }, /* @@ -396,9 +419,9 @@ var Raven = { * @return {Raven} */ setShouldSendCallback: function(callback) { - globalOptions.shouldSendCallback = callback; + this._globalOptions.shouldSendCallback = callback; - return Raven; + return this; }, /** @@ -411,9 +434,9 @@ var Raven = { * @return {Raven} */ setTransport: function(transport) { - globalOptions.transport = transport; + this._globalOptions.transport = transport; - return Raven; + return this; }, /* @@ -422,7 +445,7 @@ var Raven = { * @return {error} */ lastException: function() { - return lastCapturedException; + return this._lastCapturedException; }, /* @@ -431,7 +454,7 @@ var Raven = { * @return {string} */ lastEventId: function() { - return lastEventId; + return this._lastEventId; }, /* @@ -440,571 +463,441 @@ var Raven = { * @return {boolean} */ isSetup: function() { - return isSetup(); - } -}; - -// Deprecations -Raven.setUser = Raven.setUserContext; -Raven.setReleaseContext = Raven.setRelease; - -function triggerEvent(eventType, options) { - // NOTE: `event` is a native browser thing, so let's avoid conflicting wiht it - var evt, key; - - if (!hasDocument) - return; - - options = options || {}; - - eventType = 'raven' + eventType.substr(0,1).toUpperCase() + eventType.substr(1); + if (!this._hasJSON) return false; // needs JSON support + if (!this._globalServer) { + if (!this.ravenNotConfiguredError) + this._logDebug('error', 'Error: Raven has not been configured.'); + this.ravenNotConfiguredError = true; + return false; + } + return true; + }, - if (document.createEvent) { - evt = document.createEvent('HTMLEvents'); - evt.initEvent(eventType, true, true); - } else { - evt = document.createEventObject(); - evt.eventType = eventType; - } + afterLoad: function () { + // TODO: remove window dependence? - for (key in options) if (hasKey(options, key)) { - evt[key] = options[key]; - } + // Attempt to initialize Raven on load + var RavenConfig = window.RavenConfig; + if (RavenConfig) { + this.config(RavenConfig.dsn, RavenConfig.config).install(); + } + }, - if (document.createEvent) { - // IE9 if standards - document.dispatchEvent(evt); - } else { - // IE8 regardless of Quirks or Standards - // IE9 if quirks - try { - document.fireEvent('on' + evt.eventType.toLowerCase(), evt); - } catch(e) {} - } -} + /**** Private functions ****/ + _triggerEvent: function(eventType, options) { + // NOTE: `event` is a native browser thing, so let's avoid conflicting wiht it + var evt, key; -var dsnKeys = 'source protocol user pass host port path'.split(' '), - dsnPattern = /^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/; + if (!this._hasDocument) + return; -function RavenConfigError(message) { - this.name = 'RavenConfigError'; - this.message = message; -} -RavenConfigError.prototype = new Error(); -RavenConfigError.prototype.constructor = RavenConfigError; - -/**** Private functions ****/ -function parseDSN(str) { - var m = dsnPattern.exec(str), - dsn = {}, - i = 7; - - try { - while (i--) dsn[dsnKeys[i]] = m[i] || ''; - } catch(e) { - throw new RavenConfigError('Invalid DSN: ' + str); - } + options = options || {}; - if (dsn.pass) - throw new RavenConfigError('Do not specify your private key in the DSN!'); + eventType = 'raven' + eventType.substr(0,1).toUpperCase() + eventType.substr(1); - return dsn; -} + if (document.createEvent) { + evt = document.createEvent('HTMLEvents'); + evt.initEvent(eventType, true, true); + } else { + evt = document.createEventObject(); + evt.eventType = eventType; + } -function isUndefined(what) { - return what === void 0; -} + for (key in options) if (hasKey(options, key)) { + evt[key] = options[key]; + } -function isFunction(what) { - return typeof what === 'function'; -} + if (document.createEvent) { + // IE9 if standards + document.dispatchEvent(evt); + } else { + // IE8 regardless of Quirks or Standards + // IE9 if quirks + try { + document.fireEvent('on' + evt.eventType.toLowerCase(), evt); + } catch(e) {} + } + }, -function isString(what) { - return objectPrototype.toString.call(what) === '[object String]'; -} + /** + * Install any queued plugins + */ + _drainPlugins: function() { + var self = this; + + // FIX ME TODO + each(this._plugins, function(_, plugin) { + var installer = plugin[0]; + var args = plugin[1]; + installer.apply(self, [self].concat(args)); + }); + }, -function isObject(what) { - return typeof what === 'object' && what !== null; -} + _parseDSN: function(str) { + var m = dsnPattern.exec(str), + dsn = {}, + i = 7; -function isEmptyObject(what) { - for (var k in what) return false; - return true; -} + try { + while (i--) dsn[dsnKeys[i]] = m[i] || ''; + } catch(e) { + throw new RavenConfigError('Invalid DSN: ' + str); + } -// Sorta yanked from https://github.com/joyent/node/blob/aa3b4b4/lib/util.js#L560 -// with some tiny modifications -function isError(what) { - return isObject(what) && - objectPrototype.toString.call(what) === '[object Error]' || - what instanceof Error; -} + if (dsn.pass) + throw new RavenConfigError('Do not specify your private key in the DSN!'); -/** - * hasKey, a better form of hasOwnProperty - * Example: hasKey(MainHostObject, property) === true/false - * - * @param {Object} host object to check property - * @param {string} key to check - */ -function hasKey(object, key) { - return objectPrototype.hasOwnProperty.call(object, key); -} + return dsn; + }, -function each(obj, callback) { - var i, j; + _handleStackInfo: function(stackInfo, options) { + var self = this; + var frames = []; - if (isUndefined(obj.length)) { - for (i in obj) { - if (hasKey(obj, i)) { - callback.call(null, i, obj[i]); - } - } - } else { - j = obj.length; - if (j) { - for (i = 0; i < j; i++) { - callback.call(null, i, obj[i]); - } + if (stackInfo.stack && stackInfo.stack.length) { + each(stackInfo.stack, function(i, stack) { + var frame = self._normalizeFrame(stack); + if (frame) { + frames.push(frame); + } + }); } - } -} - -function handleStackInfo(stackInfo, options) { - var frames = []; - if (stackInfo.stack && stackInfo.stack.length) { - each(stackInfo.stack, function(i, stack) { - var frame = normalizeFrame(stack); - if (frame) { - frames.push(frame); - } + this._triggerEvent('handle', { + stackInfo: stackInfo, + options: options }); - } - - triggerEvent('handle', { - stackInfo: stackInfo, - options: options - }); - - processException( - stackInfo.name, - stackInfo.message, - stackInfo.url, - stackInfo.lineno, - frames, - options - ); -} - -function normalizeFrame(frame) { - if (!frame.url) return; - - // normalize the frames data - var normalized = { - filename: frame.url, - lineno: frame.line, - colno: frame.column, - 'function': frame.func || '?' - }, context = extractContextFromFrame(frame), i; - - if (context) { - var keys = ['pre_context', 'context_line', 'post_context']; - i = 3; - while (i--) normalized[keys[i]] = context[i]; - } - normalized.in_app = !( // determine if an exception came from outside of our app - // first we check the global includePaths list. - (!!globalOptions.includePaths.test && !globalOptions.includePaths.test(normalized.filename)) || - // Now we check for fun, if the function name is Raven or TraceKit - /(Raven|TraceKit)\./.test(normalized['function']) || - // finally, we do a last ditch effort and check for raven.min.js - /raven\.(min\.)?js$/.test(normalized.filename) - ); + this._processException( + stackInfo.name, + stackInfo.message, + stackInfo.url, + stackInfo.lineno, + frames, + options + ); + }, - return normalized; -} + _normalizeFrame: function(frame) { + if (!frame.url) return; + + // normalize the frames data + var normalized = { + filename: frame.url, + lineno: frame.line, + colno: frame.column, + 'function': frame.func || '?' + }, context = this._extractContextFromFrame(frame), i; + + if (context) { + var keys = ['pre_context', 'context_line', 'post_context']; + i = 3; + while (i--) normalized[keys[i]] = context[i]; + } -function extractContextFromFrame(frame) { - // immediately check if we should even attempt to parse a context - if (!frame.context || !globalOptions.fetchContext) return; + normalized.in_app = !( // determine if an exception came from outside of our app + // first we check the global includePaths list. + (!!this._globalOptions.includePaths.test && !this._globalOptions.includePaths.test(normalized.filename)) || + // Now we check for fun, if the function name is Raven or TraceKit + /(Raven|TraceKit)\./.test(normalized['function']) || + // finally, we do a last ditch effort and check for raven.min.js + /raven\.(min\.)?js$/.test(normalized.filename) + ); - var context = frame.context, - pivot = ~~(context.length / 2), - i = context.length, isMinified = false; + return normalized; + }, - while (i--) { - // We're making a guess to see if the source is minified or not. - // To do that, we make the assumption if *any* of the lines passed - // in are greater than 300 characters long, we bail. - // Sentry will see that there isn't a context - if (context[i].length > 300) { - isMinified = true; - break; + _extractContextFromFrame: function(frame) { + // immediately check if we should even attempt to parse a context + if (!frame.context || !this._globalOptions.fetchContext) return; + + var context = frame.context, + pivot = ~~(context.length / 2), + i = context.length, isMinified = false; + + while (i--) { + // We're making a guess to see if the source is minified or not. + // To do that, we make the assumption if *any* of the lines passed + // in are greater than 300 characters long, we bail. + // Sentry will see that there isn't a context + if (context[i].length > 300) { + isMinified = true; + break; + } } - } - if (isMinified) { - // The source is minified and we don't know which column. Fuck it. - if (isUndefined(frame.column)) return; + if (isMinified) { + // The source is minified and we don't know which column. Fuck it. + if (isUndefined(frame.column)) return; + + // If the source is minified and has a frame column + // we take a chunk of the offending line to hopefully shed some light + return [ + [], // no pre_context + context[pivot].substr(frame.column, 50), // grab 50 characters, starting at the offending column + [] // no post_context + ]; + } - // If the source is minified and has a frame column - // we take a chunk of the offending line to hopefully shed some light return [ - [], // no pre_context - context[pivot].substr(frame.column, 50), // grab 50 characters, starting at the offending column - [] // no post_context + context.slice(0, pivot), // pre_context + context[pivot], // context_line + context.slice(pivot + 1) // post_context ]; - } - - return [ - context.slice(0, pivot), // pre_context - context[pivot], // context_line - context.slice(pivot + 1) // post_context - ]; -} - -function processException(type, message, fileurl, lineno, frames, options) { - var stacktrace, i, fullMessage; - - if (!!globalOptions.ignoreErrors.test && globalOptions.ignoreErrors.test(message)) return; - - message += ''; - fullMessage = type + ': ' + message; - - if (frames && frames.length) { - fileurl = frames[0].filename || fileurl; - // Sentry expects frames oldest to newest - // and JS sends them as newest to oldest - frames.reverse(); - stacktrace = {frames: frames}; - } else if (fileurl) { - stacktrace = { - frames: [{ - filename: fileurl, - lineno: lineno, - in_app: true - }] - }; - } + }, - if (!!globalOptions.ignoreUrls.test && globalOptions.ignoreUrls.test(fileurl)) return; - if (!!globalOptions.whitelistUrls.test && !globalOptions.whitelistUrls.test(fileurl)) return; - - // Fire away! - send( - objectMerge({ - // sentry.interfaces.Exception - exception: { - values: [{ - type: type, - value: message, - stacktrace: stacktrace + _processException: function(type, message, fileurl, lineno, frames, options) { + var stacktrace, i, fullMessage; + + if (!!this._globalOptions.ignoreErrors.test && this._globalOptions.ignoreErrors.test(message)) return; + + message += ''; + message = truncate(message, this._globalOptions.maxMessageLength); + + fullMessage = type + ': ' + message; + fullMessage = truncate(fullMessage, this._globalOptions.maxMessageLength); + + if (frames && frames.length) { + fileurl = frames[0].filename || fileurl; + // Sentry expects frames oldest to newest + // and JS sends them as newest to oldest + frames.reverse(); + stacktrace = {frames: frames}; + } else if (fileurl) { + stacktrace = { + frames: [{ + filename: fileurl, + lineno: lineno, + in_app: true }] - }, - culprit: fileurl, - message: fullMessage - }, options) - ); -} - -function objectMerge(obj1, obj2) { - if (!obj2) { - return obj1; - } - each(obj2, function(key, value){ - obj1[key] = value; - }); - return obj1; -} - -function truncate(str, max) { - return str.length <= max ? str : str.substr(0, max) + '\u2026'; -} + }; + } -function trimPacket(data) { - // For now, we only want to truncate the two different messages - // but this could/should be expanded to just trim everything - var max = globalOptions.maxMessageLength; - data.message = truncate(data.message, max); - if (data.exception) { - var exception = data.exception.values[0]; - exception.value = truncate(exception.value, max); - } + if (!!this._globalOptions.ignoreUrls.test && this._globalOptions.ignoreUrls.test(fileurl)) return; + if (!!this._globalOptions.whitelistUrls.test && !this._globalOptions.whitelistUrls.test(fileurl)) return; - return data; -} + // Fire away! + this._send( + objectMerge({ + // sentry.interfaces.Exception + exception: { + values: [{ + type: type, + value: message, + stacktrace: stacktrace + }] + }, + culprit: fileurl, + message: fullMessage + }, options) + ); + }, -function now() { - return +new Date(); -} + _trimPacket: function(data) { + // For now, we only want to truncate the two different messages + // but this could/should be expanded to just trim everything + var max = this._globalOptions.maxMessageLength; + data.message = truncate(data.message, max); + if (data.exception) { + var exception = data.exception.values[0]; + exception.value = truncate(exception.value, max); + } -function getHttpData() { - if (!hasDocument || !document.location || !document.location.href) { - return; - } + return data; + }, - var httpData = { - headers: { - 'User-Agent': navigator.userAgent + _getHttpData: function() { + if (!this._hasDocument || !document.location || !document.location.href) { + return; } - }; - - httpData.url = document.location.href; - if (document.referrer) { - httpData.headers.Referer = document.referrer; - } + var httpData = { + headers: { + 'User-Agent': navigator.userAgent + } + }; - return httpData; -} + httpData.url = document.location.href; -function send(data) { - var baseData = { - project: globalProject, - logger: globalOptions.logger, - platform: 'javascript' - }, httpData = getHttpData(); + if (document.referrer) { + httpData.headers.Referer = document.referrer; + } - if (httpData) { - baseData.request = httpData; - } + return httpData; + }, - data = objectMerge(baseData, data); - // Merge in the tags and extra separately since objectMerge doesn't handle a deep merge - data.tags = objectMerge(objectMerge({}, globalContext.tags), data.tags); - data.extra = objectMerge(objectMerge({}, globalContext.extra), data.extra); + _send: function(data) { + var self = this; - // Send along our own collected metadata with extra - data.extra['session:duration'] = now() - startTime; + var globalOptions = this._globalOptions; - // If there are no tags/extra, strip the key from the payload alltogther. - if (isEmptyObject(data.tags)) delete data.tags; + var baseData = { + project: this._globalProject, + logger: globalOptions.logger, + platform: 'javascript' + }, httpData = this._getHttpData(); - if (globalContext.user) { - // sentry.interfaces.User - data.user = globalContext.user; - } + if (httpData) { + baseData.request = httpData; + } - // Include the release if it's defined in globalOptions - if (globalOptions.release) data.release = globalOptions.release; - // Include server_name if it's defined in globalOptions - if (globalOptions.serverName) data.server_name = globalOptions.serverName; + data = objectMerge(baseData, data); - if (isFunction(globalOptions.dataCallback)) { - data = globalOptions.dataCallback(data) || data; - } + // Merge in the tags and extra separately since objectMerge doesn't handle a deep merge + data.tags = objectMerge(objectMerge({}, this._globalContext.tags), data.tags); + data.extra = objectMerge(objectMerge({}, this._globalContext.extra), data.extra); - // Why?????????? - if (!data || isEmptyObject(data)) { - return; - } + // Send along our own collected metadata with extra + data.extra['session:duration'] = now() - this._startTime; - // Check if the request should be filtered or not - if (isFunction(globalOptions.shouldSendCallback) && !globalOptions.shouldSendCallback(data)) { - return; - } + // If there are no tags/extra, strip the key from the payload alltogther. + if (isEmptyObject(data.tags)) delete data.tags; - // Send along an event_id if not explicitly passed. - // This event_id can be used to reference the error within Sentry itself. - // Set lastEventId after we know the error should actually be sent - lastEventId = data.event_id || (data.event_id = uuid4()); - - // Try and clean up the packet before sending by truncating long values - data = trimPacket(data); - - logDebug('debug', 'Raven about to send:', data); - - if (!isSetup()) return; - - (globalOptions.transport || makeRequest)({ - url: globalServer, - auth: { - sentry_version: '7', - sentry_client: 'raven-js/' + Raven.VERSION, - sentry_key: globalKey - }, - data: data, - options: globalOptions, - onSuccess: function success() { - triggerEvent('success', { - data: data, - src: globalServer - }); - }, - onError: function failure() { - triggerEvent('failure', { - data: data, - src: globalServer - }); + if (this._globalContext.user) { + // sentry.interfaces.User + data.user = this._globalContext.user; } - }); -} -function makeImageRequest(opts) { - // Tack on sentry_data to auth options, which get urlencoded - opts.auth.sentry_data = JSON.stringify(opts.data); + // Include the release if it's defined in globalOptions + if (globalOptions.release) data.release = globalOptions.release; + // Include server_name if it's defined in globalOptions + if (globalOptions.serverName) data.server_name = globalOptions.serverName; - var img = newImage(), - src = opts.url + '?' + urlencode(opts.auth), - crossOrigin = opts.options.crossOrigin; + // Include the release if it's defined in globalOptions + if (globalOptions.release) data.release = globalOptions.release; - if (crossOrigin || crossOrigin === '') { - img.crossOrigin = crossOrigin; - } - img.onload = opts.onSuccess; - img.onerror = img.onabort = opts.onError; - img.src = src; -} + if (isFunction(globalOptions.dataCallback)) { + data = globalOptions.dataCallback(data) || data; + } -function makeXhrRequest(opts) { - var request; + // Why?????????? + if (!data || isEmptyObject(data)) { + return; + } - function handler() { - if (request.status === 200) { - if (opts.onSuccess) { - opts.onSuccess(); - } - } else if (opts.onError) { - opts.onError(); + // Check if the request should be filtered or not + if (isFunction(globalOptions.shouldSendCallback) && !globalOptions.shouldSendCallback(data)) { + return; } - } - request = new XMLHttpRequest(); - if ('withCredentials' in request) { - request.onreadystatechange = function () { - if (request.readyState !== 4) { - return; - } - handler(); - }; - } else { - request = new XDomainRequest(); - // onreadystatechange not supported by XDomainRequest - request.onload = handler; - } + // Send along an event_id if not explicitly passed. + // This event_id can be used to reference the error within Sentry itself. + // Set lastEventId after we know the error should actually be sent + this._lastEventId = data.event_id || (data.event_id = uuid4()); - // NOTE: auth is intentionally sent as part of query string (NOT as custom - // HTTP header) so as to avoid preflight CORS requests - request.open('POST', opts.url + '?' + urlencode(opts.auth)); - request.send(JSON.stringify(opts.data)); -} + // Try and clean up the packet before sending by truncating long values + data = this._trimPacket(data); -function makeRequest(opts) { - var hasCORS = - 'withCredentials' in new XMLHttpRequest() || - typeof XDomainRequest !== 'undefined'; + this._logDebug('debug', 'Raven about to send:', data); - return (hasCORS ? makeXhrRequest : makeImageRequest)(opts); -} + if (!this.isSetup()) return; -// Note: this is shitty, but I can't figure out how to get -// sinon to stub document.createElement without breaking everything -// so this wrapper is just so I can stub it for tests. -function newImage() { - return document.createElement('img'); -} + (globalOptions.transport || this._makeRequest).call(this, { + url: this._globalServer, + auth: { + sentry_version: '7', + sentry_client: 'raven-js/' + this.VERSION, + sentry_key: this._globalKey + }, + data: data, + options: globalOptions, + onSuccess: function success() { + self._triggerEvent('success', { + data: data, + src: self._globalServer + }); + }, + onError: function failure() { + self._triggerEvent('failure', { + data: data, + src: self._globalServer + }); + } + }); + }, -var ravenNotConfiguredError; + _makeImageRequest: function(opts) { + // Tack on sentry_data to auth options, which get urlencoded + opts.auth.sentry_data = JSON.stringify(opts.data); -function isSetup() { - if (!hasJSON) return false; // needs JSON support - if (!globalServer) { - if (!ravenNotConfiguredError) - logDebug('error', 'Error: Raven has not been configured.'); - ravenNotConfiguredError = true; - return false; - } - return true; -} + var img = this._newImage(), + src = opts.url + '?' + urlencode(opts.auth), + crossOrigin = opts.options.crossOrigin; -function joinRegExp(patterns) { - // Combine an array of regular expressions and strings into one large regexp - // Be mad. - var sources = [], - i = 0, len = patterns.length, - pattern; - - for (; i < len; i++) { - pattern = patterns[i]; - if (isString(pattern)) { - // If it's a string, we need to escape it - // Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions - sources.push(pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1")); - } else if (pattern && pattern.source) { - // If it's a regexp already, we want to extract the source - sources.push(pattern.source); - } - // Intentionally skip other cases - } - return new RegExp(sources.join('|'), 'i'); -} + if (crossOrigin || crossOrigin === '') { + img.crossOrigin = crossOrigin; + } + img.onload = opts.onSuccess; + img.onerror = img.onabort = opts.onError; + img.src = src; + }, -function uuid4() { - var crypto = window.crypto || window.msCrypto; + _makeXhrRequest: function(opts) { + var request; - if (!isUndefined(crypto) && crypto.getRandomValues) { - // Use window.crypto API if available - var arr = new Uint16Array(8); - crypto.getRandomValues(arr); + function handler() { + if (request.status === 200) { + if (opts.onSuccess) { + opts.onSuccess(); + } + } else if (opts.onError) { + opts.onError(); + } + } - // set 4 in byte 7 - arr[3] = arr[3] & 0xFFF | 0x4000; - // set 2 most significant bits of byte 9 to '10' - arr[4] = arr[4] & 0x3FFF | 0x8000; + request = new XMLHttpRequest(); + if ('withCredentials' in request) { + request.onreadystatechange = function () { + if (request.readyState !== 4) { + return; + } + handler(); + }; + } else { + request = new XDomainRequest(); + // onreadystatechange not supported by XDomainRequest + request.onload = handler; + } - var pad = function(num) { - var v = num.toString(16); - while (v.length < 4) { - v = '0' + v; - } - return v; - }; + // NOTE: auth is intentionally sent as part of query string (NOT as custom + // HTTP header) so as to avoid preflight CORS requests + request.open('POST', opts.url + '?' + urlencode(opts.auth)); + request.send(JSON.stringify(opts.data)); + }, - return (pad(arr[0]) + pad(arr[1]) + pad(arr[2]) + pad(arr[3]) + pad(arr[4]) + - pad(arr[5]) + pad(arr[6]) + pad(arr[7])); - } else { - // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 - return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, - v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); - } -} + _makeRequest: function(opts) { + var hasCORS = + 'withCredentials' in new XMLHttpRequest() || + typeof XDomainRequest !== 'undefined'; -function logDebug(level) { - if (originalConsoleMethods[level] && Raven.debug) { - // _slice is coming from vendor/TraceKit/tracekit.js - // so it's accessible globally - originalConsoleMethods[level].apply(originalConsole, _slice.call(arguments, 1)); - } -} + return (hasCORS ? this._makeXhrRequest : this._makeImageRequest)(opts); + }, -function afterLoad() { - // Attempt to initialize Raven on load - var RavenConfig = window.RavenConfig; - if (RavenConfig) { - Raven.config(RavenConfig.dsn, RavenConfig.config).install(); - } -} + // Note: this is shitty, but I can't figure out how to get + // sinon to stub document.createElement without breaking everything + // so this wrapper is just so I can stub it for tests. + _newImage: function() { + return document.createElement('img'); + }, -function urlencode(o) { - var pairs = []; - each(o, function(key, value) { - pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); - }); - return pairs.join('&'); -} + _logDebug: function(level) { + if (this._originalConsoleMethods[level] && this.debug) { + this._originalConsoleMethods[level].apply(this._originalConsole, [].slice.call(arguments, 1)); + } + }, -function mergeContext(key, context) { - if (isUndefined(context)) { - delete globalContext[key]; - } else { - globalContext[key] = objectMerge(globalContext[key] || {}, context); + _mergeContext: function(key, context) { + if (isUndefined(context)) { + delete this._globalContext[key]; + } else { + this._globalContext[key] = objectMerge(this._globalContext[key] || {}, context); + } } -} +}; + +// Deprecations +Raven.prototype.setUser = Raven.prototype.setUserContext; +Raven.prototype.setReleaseContext = Raven.prototype.setRelease; -afterLoad(); +module.exports = Raven; diff --git a/src/singleton.js b/src/singleton.js new file mode 100644 index 000000000000..48cee938b016 --- /dev/null +++ b/src/singleton.js @@ -0,0 +1,28 @@ +/** + * Enforces a single instance of the Raven client, and the + * main entry point for Raven. If you are a consumer of the + * Raven library, you SHOULD load this file (vs raven.js). + **/ + +'use strict'; + +var RavenConstructor = require('./raven'); + +var _Raven = window.Raven; + +var Raven = new RavenConstructor(); + +/* + * Allow multiple versions of Raven to be installed. + * Strip Raven from the global context and returns the instance. + * + * @return {Raven} + */ +Raven.noConflict = function () { + window.Raven = _Raven; + return Raven; +}; + +Raven.afterLoad(); + +module.exports = Raven; diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 000000000000..21da2fcad3cc --- /dev/null +++ b/src/utils.js @@ -0,0 +1,155 @@ +'use strict'; + +var objectPrototype = Object.prototype; + +function isUndefined(what) { + return what === void 0; +} + +function isFunction(what) { + return typeof what === 'function'; +} + +function isString(what) { + return objectPrototype.toString.call(what) === '[object String]'; +} + +function isObject(what) { + return typeof what === 'object' && what !== null; +} + +function isEmptyObject(what) { + for (var k in what) return false; + return true; +} + +// Sorta yanked from https://github.com/joyent/node/blob/aa3b4b4/lib/util.js#L560 +// with some tiny modifications +function isError(what) { + return isObject(what) && + objectPrototype.toString.call(what) === '[object Error]' || + what instanceof Error; +} + +function each(obj, callback) { + var i, j; + + if (isUndefined(obj.length)) { + for (i in obj) { + if (hasKey(obj, i)) { + callback.call(null, i, obj[i]); + } + } + } else { + j = obj.length; + if (j) { + for (i = 0; i < j; i++) { + callback.call(null, i, obj[i]); + } + } + } +} + +function objectMerge(obj1, obj2) { + if (!obj2) { + return obj1; + } + each(obj2, function(key, value){ + obj1[key] = value; + }); + return obj1; +} + +function truncate(str, max) { + return str.length <= max ? str : str.substr(0, max) + '\u2026'; +} + +/** + * hasKey, a better form of hasOwnProperty + * Example: hasKey(MainHostObject, property) === true/false + * + * @param {Object} host object to check property + * @param {string} key to check + */ +function hasKey(object, key) { + return objectPrototype.hasOwnProperty.call(object, key); +} + +function joinRegExp(patterns) { + // Combine an array of regular expressions and strings into one large regexp + // Be mad. + var sources = [], + i = 0, len = patterns.length, + pattern; + + for (; i < len; i++) { + pattern = patterns[i]; + if (isString(pattern)) { + // If it's a string, we need to escape it + // Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + sources.push(pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1")); + } else if (pattern && pattern.source) { + // If it's a regexp already, we want to extract the source + sources.push(pattern.source); + } + // Intentionally skip other cases + } + return new RegExp(sources.join('|'), 'i'); +} + +function urlencode(o) { + var pairs = []; + each(o, function(key, value) { + pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + }); + return pairs.join('&'); +} + +function uuid4() { + var crypto = window.crypto || window.msCrypto; + + if (!isUndefined(crypto) && crypto.getRandomValues) { + // Use window.crypto API if available + var arr = new Uint16Array(8); + crypto.getRandomValues(arr); + + // set 4 in byte 7 + arr[3] = arr[3] & 0xFFF | 0x4000; + // set 2 most significant bits of byte 9 to '10' + arr[4] = arr[4] & 0x3FFF | 0x8000; + + var pad = function(num) { + var v = num.toString(16); + while (v.length < 4) { + v = '0' + v; + } + return v; + }; + + return (pad(arr[0]) + pad(arr[1]) + pad(arr[2]) + pad(arr[3]) + pad(arr[4]) + + pad(arr[5]) + pad(arr[6]) + pad(arr[7])); + } else { + // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 + return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, + v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + } +} + +module.exports = { + isUndefined: isUndefined, + isFunction: isFunction, + isString: isString, + isObject: isObject, + isEmptyObject: isEmptyObject, + isError: isError, + each: each, + objectMerge: objectMerge, + truncate: truncate, + hasKey: hasKey, + joinRegExp: joinRegExp, + urlencode: urlencode, + uuid4: uuid4 +}; diff --git a/template/_footer.js b/template/_footer.js deleted file mode 100644 index b6583ef31388..000000000000 --- a/template/_footer.js +++ /dev/null @@ -1,19 +0,0 @@ -// This is being exposed no matter what because there are too many weird -// usecases for how people use Raven. If this is really a problem, I'm sorry. -window.Raven = Raven; - -// Expose Raven to the world -if (typeof define === 'function' && define.amd) { - // AMD - define('raven', [], function() { - return Raven; - }); -} else if (typeof module === 'object') { - // browserify - module.exports = Raven; -} else if (typeof exports === 'object') { - // CommonJS - exports = Raven; -} - -})(typeof window !== 'undefined' ? window : this); diff --git a/template/_header.js b/template/_header.js deleted file mode 100644 index 27595a61a2db..000000000000 --- a/template/_header.js +++ /dev/null @@ -1,2 +0,0 @@ -;(function(window, undefined){ -'use strict'; diff --git a/test/index.html b/test/index.html index d988e2313b9c..74d2a67dc6f6 100644 --- a/test/index.html +++ b/test/index.html @@ -19,6 +19,7 @@ - - - - - - - +