'use strict'; // 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), lastCapturedException, lastEventId, globalServer, globalUser, globalKey, globalProject, globalOptions = { logger: 'javascript', ignoreErrors: [], ignoreUrls: [], whitelistUrls: [], includePaths: [], collectWindowErrors: true, tags: {}, maxMessageLength: 100, extra: {} }, authQueryString, isRavenInstalled = false, objectPrototype = Object.prototype, startTime = now(); /* * The core Raven singleton * * @this {Raven} */ var Raven = { VERSION: '<%= pkg.version %>', debug: true, /* * 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; }, /* * Configure Raven with a DSN and extra options * * @param {string} dsn The public Sentry DSN * @param {object} options Optional set of of global options [optional] * @return {Raven} */ config: function(dsn, options) { if (globalServer) { logDebug('error', 'Error: Raven has already been configured'); return Raven; } if (!dsn) return Raven; var uri = parseDSN(dsn), lastSlash = uri.path.lastIndexOf('/'), path = uri.path.substr(1, lastSlash); // merge in options if (options) { each(options, function(key, value){ 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$/); // 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); globalKey = uri.user; globalProject = uri.path.substr(lastSlash + 1); // assemble the endpoint from the uri pieces globalServer = '//' + uri.host + (uri.port ? ':' + uri.port : '') + '/' + path + 'api/' + globalProject + '/store/'; if (uri.protocol) { globalServer = uri.protocol + ':' + globalServer; } if (globalOptions.fetchContext) { TraceKit.remoteFetching = true; } if (globalOptions.linesOfContext) { TraceKit.linesOfContext = globalOptions.linesOfContext; } TraceKit.collectWindowErrors = !!globalOptions.collectWindowErrors; setAuthQueryString(); // return for chaining return Raven; }, /* * Installs a global window.onerror error handler * to capture and report uncaught exceptions. * At this point, install() is required to be called due * to the way TraceKit is set up. * * @return {Raven} */ install: function() { if (isSetup() && !isRavenInstalled) { TraceKit.report.subscribe(handleStackInfo); isRavenInstalled = true; } return Raven; }, /* * Wrap code within a context so Raven can capture errors * reliably across domains that is executed immediately. * * @param {object} options A specific set of options for this context [optional] * @param {function} func The callback to be immediately executed within the context * @param {array} args An array of arguments to be called with the callback [optional] */ context: function(options, func, args) { if (isFunction(options)) { args = func || []; func = options; options = undefined; } return Raven.wrap(options, func).apply(this, args); }, /* * Wrap code within a context and returns back a new function to be executed * * @param {object} options A specific set of options for this context [optional] * @param {function} func The function to be wrapped in a new context * @return {function} The newly wrapped functions with a context */ wrap: function(options, func) { // 1 argument has been passed, and it's not a function // so just return it if (isUndefined(func) && !isFunction(options)) { return options; } // options is optional if (isFunction(options)) { func = options; options = undefined; } // At this point, we've passed along 2 arguments, and the second one // is not a function either, so we'll just return the second argument. if (!isFunction(func)) { return func; } // We don't wanna wrap it twice! if (func.__raven__) { return func; } function wrapped() { var args = [], i = arguments.length, deep = !options || options && options.deep !== false; // Recursively wrap all of a function's arguments that are // functions themselves. while(i--) args[i] = deep ? Raven.wrap(options, arguments[i]) : arguments[i]; try { /*jshint -W040*/ return func.apply(this, args); } catch(e) { Raven.captureException(e, options); throw e; } } // copy over properties of the old function for (var property in func) { if (hasKey(func, property)) { wrapped[property] = func[property]; } } // Signal that this function has been wrapped already // for both debugging and to prevent it to being wrapped twice wrapped.__raven__ = true; wrapped.__inner__ = func; return wrapped; }, /* * Uninstalls the global error handler. * * @return {Raven} */ uninstall: function() { TraceKit.report.uninstall(); isRavenInstalled = false; return Raven; }, /* * Manually capture an exception and send it over to Sentry * * @param {error} ex An exception to be logged * @param {object} options A specific set of options for this error [optional] * @return {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); // Store the raw exception object for potential debugging and introspection lastCapturedException = ex; // TraceKit.report will re-raise any exception passed to it, // which means you have to wrap it in try/catch. Instead, we // can wrap it here and only re-raise if TraceKit.report // raises an exception different from the one we asked to // report on. try { TraceKit.report(ex, options); } catch(ex1) { if(ex !== ex1) { throw ex1; } } return Raven; }, /* * Manually send a message to Sentry * * @param {string} msg A plain message to be captured in Sentry * @param {object} options A specific set of options for this message [optional] * @return {Raven} */ captureMessage: function(msg, options) { // 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)) { return; } // Fire away! send( objectMerge({ message: msg + '' // Make sure it's actually a string }, options) ); return Raven; }, /* * Set/clear a user to be sent along with the payload. * * @param {object} user An object representing user data [optional] * @return {Raven} */ setUserContext: function(user) { globalUser = user; return Raven; }, /* * Set extra attributes to be sent along with the payload. * * @param {object} extra An object representing extra data [optional] * @return {Raven} */ setExtraContext: function(extra) { globalOptions.extra = extra || {}; return Raven; }, /* * Set tags to be sent along with the payload. * * @param {object} tags An object representing tags [optional] * @return {Raven} */ setTagsContext: function(tags) { globalOptions.tags = tags || {}; return Raven; }, /* * Set release version of application * * @param {string} release Typically something like a git SHA to identify version * @return {Raven} */ setReleaseContext: function(release) { globalOptions.release = release; return Raven; }, /* * Set the dataCallback option * * @param {function} callback The callback to run which allows the * data blob to be mutated before sending * @return {Raven} */ setDataCallback: function(callback) { globalOptions.dataCallback = callback; return Raven; }, /* * Set the shouldSendCallback option * * @param {function} callback The callback to run which allows * introspecting the blob before sending * @return {Raven} */ setShouldSendCallback: function(callback) { globalOptions.shouldSendCallback = callback; return Raven; }, /* * Get the latest raw exception that was captured by Raven. * * @return {error} */ lastException: function() { return lastCapturedException; }, /* * Get the last event id * * @return {string} */ lastEventId: function() { return lastEventId; }, /* * Determine if Raven is setup and ready to go. * * @return {boolean} */ isSetup: function() { return isSetup(); } }; Raven.setUser = Raven.setUserContext; // To be deprecated function triggerEvent(eventType, options) { var event, key; options = options || {}; eventType = 'raven' + eventType.substr(0,1).toUpperCase() + eventType.substr(1); if (document.createEvent) { event = document.createEvent('HTMLEvents'); event.initEvent(eventType, true, true); } else { event = document.createEventObject(); event.eventType = eventType; } for (key in options) if (hasKey(options, key)) { event[key] = options[key]; } if (document.createEvent) { // IE9 if standards document.dispatchEvent(event); } else { // IE8 regardless of Quirks or Standards // IE9 if quirks try { document.fireEvent('on' + event.eventType.toLowerCase(), event); } catch(e) {} } } var dsnKeys = 'source protocol user pass host port path'.split(' '), dsnPattern = /^(?:(\w+):)?\/\/(\w+)(:\w+)?@([\w\.-]+)(?::(\d+))?(\/.*)/; 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); } if (dsn.pass) throw new RavenConfigError('Do not specify your private key in the DSN!'); return dsn; } 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; } /** * 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 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 setAuthQueryString() { authQueryString = '?sentry_version=4' + '&sentry_client=raven-js/' + Raven.VERSION + '&sentry_key=' + globalKey; } 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); } }); } 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(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) ); return normalized; } function extractContextFromFrame(frame) { // immediately check if we should even attempt to parse a context if (!frame.context || !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 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 ]; } 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, label, i; // In some instances message is not actually a string, no idea why, // so we want to always coerce it to one. message += ''; // Sometimes an exception is getting logged in Sentry as // // This can only mean that the message was falsey since this value // is hardcoded into Sentry itself. // At this point, if the message is falsey, we bail since it's useless if (type === 'Error' && !message) return; if (globalOptions.ignoreErrors.test(message)) return; 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 }] }; } // Truncate the message to a max of characters message = truncate(message, globalOptions.maxMessageLength); if (globalOptions.ignoreUrls && globalOptions.ignoreUrls.test(fileurl)) return; if (globalOptions.whitelistUrls && !globalOptions.whitelistUrls.test(fileurl)) return; label = lineno ? message + ' at ' + lineno : message; // Fire away! send( objectMerge({ // sentry.interfaces.Exception exception: { type: type, value: message }, // sentry.interfaces.Stacktrace stacktrace: stacktrace, culprit: fileurl, message: label }, 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 now() { return +new Date(); } function getHttpData() { var http = { url: document.location.href, headers: { 'User-Agent': navigator.userAgent } }; if (document.referrer) { http.headers.Referer = document.referrer; } return http; } function send(data) { if (!isSetup()) return; data = objectMerge({ project: globalProject, logger: globalOptions.logger, platform: 'javascript', // sentry.interfaces.Http request: getHttpData() }, data); // Merge in the tags and extra separately since objectMerge doesn't handle a deep merge data.tags = objectMerge(objectMerge({}, globalOptions.tags), data.tags); data.extra = objectMerge(objectMerge({}, globalOptions.extra), data.extra); // Send along our own collected metadata with extra data.extra = objectMerge({ 'session:duration': now() - startTime }, data.extra); // If there are no tags/extra, strip the key from the payload alltogther. if (isEmptyObject(data.tags)) delete data.tags; if (globalUser) { // sentry.interfaces.User data.user = globalUser; } // Include the release iff it's defined in globalOptions if (globalOptions.release) data.release = globalOptions.release; if (isFunction(globalOptions.dataCallback)) { data = globalOptions.dataCallback(data) || data; } // Why?????????? if (!data || isEmptyObject(data)) { return; } // Check if the request should be filtered or not if (isFunction(globalOptions.shouldSendCallback) && !globalOptions.shouldSendCallback(data)) { return; } // 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()); makeRequest(data); } function makeRequest(data) { var img = newImage(), src = globalServer + authQueryString + '&sentry_data=' + encodeURIComponent(JSON.stringify(data)); img.crossOrigin = 'anonymous'; img.onload = function success() { triggerEvent('success', { data: data, src: src }); }; img.onerror = img.onabort = function failure() { triggerEvent('failure', { data: data, src: src }); }; img.src = src; } // 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'); } function isSetup() { if (!hasJSON) return false; // needs JSON support if (!globalServer) { logDebug('error', 'Error: Raven has not been configured.'); return false; } return true; } 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'); } // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 function uuid4() { return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }); } function logDebug(level, message) { if (window.console && console[level] && Raven.debug) { console[level](message); } } function afterLoad() { // Attempt to initialize Raven on load var RavenConfig = window.RavenConfig; if (RavenConfig) { Raven.config(RavenConfig.dsn, RavenConfig.config).install(); } } afterLoad();