diff --git a/Gruntfile.js b/Gruntfile.js index adcf3d982c4c..407dfee6c6b1 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -132,7 +132,7 @@ module.exports = function(grunt) { dest: 'build/raven.test.js', options: { browserifyOptions: { - debug: true // source maps + debug: false// source maps }, ignore: ['react-native'], plugin: [proxyquire.plugin] diff --git a/example/index.html b/example/index.html index 76b237e41d67..11ff74e3511a 100644 --- a/example/index.html +++ b/example/index.html @@ -36,6 +36,7 @@ + diff --git a/example/scratch.js b/example/scratch.js index 794511ed4b2c..48507f9c5d1f 100644 --- a/example/scratch.js +++ b/example/scratch.js @@ -46,6 +46,12 @@ function showDialog() { Raven.showReportDialog(); } +function testSynthetic() { + Raven.captureMessage('synthetic', { + stacktrace: true + }); +} + function blobExample() { var xhr = new XMLHttpRequest(); xhr.open('GET', 'stack.js'); diff --git a/src/raven.js b/src/raven.js index 0cad3335395e..c8aa18fefc40 100644 --- a/src/raven.js +++ b/src/raven.js @@ -325,7 +325,12 @@ Raven.prototype = { */ captureException: function(ex, options) { // If not an Error is passed through, recall as a message instead - if (!isError(ex)) return this.captureMessage(ex, options); + if (!isError(ex)) { + return this.captureMessage(ex, objectMerge({ + trimHeadFrames: 1, + stacktrace: true // if we fall back to captureMessage, default to attempting a new trace + }, options)); + } // Store the raw exception object for potential debugging and introspection this._lastCapturedException = ex; @@ -362,12 +367,41 @@ Raven.prototype = { return; } + var data = objectMerge({ + message: msg + '' // Make sure it's actually a string + }, options); + + if (options && options.stacktrace) { + var ex; + // create a stack trace from this point; just trim + // off extra frames so they don't include this function call (or + // earlier Raven.js library fn calls) + try { + throw new Error(msg); + } catch (ex1) { + ex = ex1; + } + + // null exception name so `Error` isn't prefixed to msg + ex.name = null; + + options = objectMerge({ + // fingerprint on msg, not stack trace (legacy behavior, could be + // revisited) + fingerprint: msg, + trimHeadFrames: (options.trimHeadFrames || 0) + 1 + }, options); + + var stack = TraceKit.computeStackTrace(ex); + var frames = this._prepareFrames(stack, options); + data.stacktrace = { + // Sentry expects frames oldest to newest + frames: frames.reverse() + } + } + // Fire away! - this._send( - objectMerge({ - message: msg + '' // Make sure it's actually a string - }, options) - ); + this._send(data); return this; }, @@ -1065,17 +1099,7 @@ Raven.prototype = { }, _handleStackInfo: function(stackInfo, options) { - var self = this; - var frames = []; - - if (stackInfo.stack && stackInfo.stack.length) { - each(stackInfo.stack, function(i, stack) { - var frame = self._normalizeFrame(stack); - if (frame) { - frames.push(frame); - } - }); - } + var frames = this._prepareFrames(stackInfo, options); this._triggerEvent('handle', { stackInfo: stackInfo, @@ -1087,11 +1111,36 @@ Raven.prototype = { stackInfo.message, stackInfo.url, stackInfo.lineno, - frames.slice(0, this._globalOptions.stackTraceLimit), + frames, options ); }, + _prepareFrames: function(stackInfo, options) { + var self = this; + var frames = []; + if (stackInfo.stack && stackInfo.stack.length) { + each(stackInfo.stack, function(i, stack) { + var frame = self._normalizeFrame(stack); + if (frame) { + frames.push(frame); + } + }); + + // e.g. frames captured via captureMessage throw + if (options && options.trimHeadFrames) { + for (var j = 0; j < options.trimHeadFrames && j < frames.length; j++) { + frames[j].in_app = false; + } + // ... delete to prevent from appearing in outbound payload + delete options.trimHeadFrames; + } + } + frames = frames.slice(0, this._globalOptions.stackTraceLimit); + return frames; + }, + + _normalizeFrame: function(frame) { if (!frame.url) return; @@ -1117,7 +1166,6 @@ Raven.prototype = { _processException: function(type, message, fileurl, lineno, frames, options) { var stacktrace; - if (!!this._globalOptions.ignoreErrors.test && this._globalOptions.ignoreErrors.test(message)) return; message += ''; diff --git a/test/raven.test.js b/test/raven.test.js index 868bdf6b30ee..c8ad6bb0ccf3 100644 --- a/test/raven.test.js +++ b/test/raven.test.js @@ -1757,7 +1757,9 @@ describe('Raven (public API)', function() { Raven.context({foo: 'bar'}, broken); }, error); assert.isTrue(Raven.captureException.called); - assert.deepEqual(Raven.captureException.lastCall.args, [error, {'foo': 'bar'}]); + assert.deepEqual(Raven.captureException.lastCall.args, [error, { + 'foo': 'bar' + }]); }); it('should capture the exception without options', function() { @@ -2022,6 +2024,29 @@ describe('Raven (public API)', function() { }); }); + it('should include a synthetic stacktrace if stacktrace:true is passed', function () { + this.sinon.stub(Raven, 'isSetup').returns(true); + this.sinon.stub(Raven, '_send'); + + function foo() { + Raven.captureMessage('foo', { + stacktrace: true + }); + } + + foo(); + var frames = Raven._send.lastCall.args[0].stacktrace.frames; + + // Raven.captureMessage + var last = frames[frames.length - 1]; + assert.isTrue(/(captureMessage|^\?)$/.test(last.function)); // loose equality check because differs per-browser + assert.equal(last.in_app, false); + + // foo + var secondLast = frames[frames.length - 2]; + assert.equal(secondLast.function, 'foo'); + assert.equal(secondLast.in_app, true); + }); }); describe('.captureException', function() {