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() {