Skip to content

Commit 583f391

Browse files
committed
Handle stacktrace interfaces
1 parent bfd7efa commit 583f391

File tree

2 files changed

+200
-53
lines changed

2 files changed

+200
-53
lines changed

src/raven.js

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,49 +1338,29 @@ Raven.prototype = {
13381338
/**
13391339
* Returns true if the in-process data payload matches the signature
13401340
* of the previously-sent data
1341-
*
1341+
*
13421342
* NOTE: This has to be done at this level because TraceKit can generate
1343-
* data from window.onerror WITHOUT an exception object (IE8, IE9,
1343+
* data from window.onerror WITHOUT an exception object (IE8, IE9,
13441344
* other old browsers). This can take the form of an "exception"
13451345
* data object with a single frame (derived from the onerror args).
13461346
*/
13471347
_isRepeatData: function (current) {
13481348
var last = this._lastData;
1349-
1349+
13501350
if (!last ||
1351-
current.message !== last.message ||
1352-
current.culprit !== last.culprit)
1353-
return false;
1354-
1355-
// If there's no stacktrace, at this point we consider
1356-
// them the same event (e.g. captureMessage)
1357-
if (!(current.exception && last.exception))
1358-
return true;
1359-
1360-
var currentException = current.exception.values[0];
1361-
var lastException = last.exception.values[0];
1362-
1363-
if (currentException.type !== lastException.type ||
1364-
currentException.value !== lastException.value)
1351+
current.message !== last.message || // defined for captureMessage
1352+
current.culprit !== last.culprit) // defined for captureException/onerror
13651353
return false;
13661354

1367-
var currentFrames = currentException.stacktrace;
1368-
var lastFrames = lastException.stacktrace;
1369-
1370-
if (currentFrames.length !== lastFrames.length)
1371-
return false;
1372-
1373-
// Iterate through every frame; bail out if anything differs
1374-
var a, b;
1375-
for (var i = 0; i < currentFrames.length; i++) {
1376-
a = currentFrames[i];
1377-
b = lastFrames[i];
1378-
if (a.filename !== b.filename ||
1379-
a.lineno !== b.lineno ||
1380-
a.colno !== b.colno ||
1381-
a['function'] !== b['function'])
1382-
return false;
1355+
// Stacktrace interface (i.e. from captureMessage)
1356+
if (current.stacktrace || last.stacktrace) {
1357+
return isSameStacktrace(current.stacktrace, last.stacktrace);
1358+
}
1359+
// Exception interface (i.e. from captureException/onerror)
1360+
else if (current.exception || last.exception) {
1361+
return isSameException(current.exception, last.exception);
13831362
}
1363+
13841364
return true;
13851365
},
13861366

@@ -1879,6 +1859,56 @@ function htmlElementAsString(elem) {
18791859
return out.join('');
18801860
}
18811861

1862+
1863+
function isOnlyOneTruthy(a, b) {
1864+
return a && !b || b && !a;
1865+
}
1866+
1867+
/**
1868+
* Returns true if the two input exception interfaces have the same content
1869+
*/
1870+
function isSameException(ex1, ex2) {
1871+
if (isOnlyOneTruthy(ex1, ex2))
1872+
return false;
1873+
1874+
ex1 = ex1.values[0];
1875+
ex2 = ex2.values[0];
1876+
1877+
if (ex1.type !== ex2.type ||
1878+
ex1.value !== ex2.value)
1879+
return false;
1880+
1881+
return isSameStacktrace(ex1.stacktrace, ex2.stacktrace);
1882+
}
1883+
1884+
/**
1885+
* Returns true if the two input stack trace interfaces have the same content
1886+
*/
1887+
function isSameStacktrace(stack1, stack2) {
1888+
if (isOnlyOneTruthy(stack1, stack2))
1889+
return false;
1890+
1891+
var frames1 = stack1.frames;
1892+
var frames2 = stack2.frames;
1893+
1894+
// Exit early if frame count differs
1895+
if (frames1.length !== frames2.length)
1896+
return false;
1897+
1898+
// Iterate through every frame; bail out if anything differs
1899+
var a, b;
1900+
for (var i = 0; i < frames1.length; i++) {
1901+
a = frames1[i];
1902+
b = frames2[i];
1903+
if (a.filename !== b.filename ||
1904+
a.lineno !== b.lineno ||
1905+
a.colno !== b.colno ||
1906+
a['function'] !== b['function'])
1907+
return false;
1908+
}
1909+
return true;
1910+
}
1911+
18821912
/**
18831913
* Polyfill a method
18841914
* @param obj object e.g. `document`

test/raven.test.js

Lines changed: 137 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2564,26 +2564,6 @@ describe('Raven (public API)', function() {
25642564
});
25652565
});
25662566

2567-
describe('._captureUrlChange', function () {
2568-
it('should create a new breadcrumb from its "from" and "to" arguments', function () {
2569-
Raven._breadcrumbs = [];
2570-
Raven._captureUrlChange('/foo', '/bar');
2571-
assert.deepEqual(Raven._breadcrumbs, [
2572-
{ category: 'navigation', timestamp: 0.1, data: { from: '/foo', to: '/bar' }}
2573-
]);
2574-
});
2575-
2576-
it('should strip protocol/host if passed URLs share the same origin as location.href', function () {
2577-
Raven._location = { href: 'http://example.com/foo' };
2578-
Raven._breadcrumbs = [];
2579-
2580-
Raven._captureUrlChange('http://example.com/foo', 'http://example.com/bar');
2581-
assert.deepEqual(Raven._breadcrumbs, [
2582-
{ category: 'navigation', timestamp: 0.1, data: { from: '/foo', to: '/bar' }}
2583-
]);
2584-
});
2585-
});
2586-
25872567
describe('.Raven.isSetup', function() {
25882568
it('should work as advertised', function() {
25892569
var isSetup = this.sinon.stub(Raven, 'isSetup');
@@ -2675,6 +2655,143 @@ describe('Raven (public API)', function() {
26752655
});
26762656
});
26772657

2658+
describe('Raven (private methods)', function () {
2659+
beforeEach(function () {
2660+
this.clock = sinon.useFakeTimers();
2661+
this.clock.tick(0); // Raven initialized at time "0"
2662+
Raven = new _Raven();
2663+
});
2664+
2665+
afterEach(function () {
2666+
this.clock.restore();
2667+
});
2668+
2669+
describe('._captureUrlChange', function () {
2670+
it('should create a new breadcrumb from its "from" and "to" arguments', function () {
2671+
this.clock.tick(100);
2672+
Raven._breadcrumbs = [];
2673+
Raven._captureUrlChange('/foo', '/bar');
2674+
assert.deepEqual(Raven._breadcrumbs, [
2675+
{ category: 'navigation', timestamp: 0.1, data: { from: '/foo', to: '/bar' }}
2676+
]);
2677+
});
2678+
2679+
it('should strip protocol/host if passed URLs share the same origin as location.href', function () {
2680+
this.clock.tick(100);
2681+
Raven._location = { href: 'http://example.com/foo' };
2682+
Raven._breadcrumbs = [];
2683+
2684+
Raven._captureUrlChange('http://example.com/foo', 'http://example.com/bar');
2685+
assert.deepEqual(Raven._breadcrumbs, [
2686+
{ category: 'navigation', timestamp: 0.1, data: { from: '/foo', to: '/bar' }}
2687+
]);
2688+
});
2689+
});
2690+
2691+
describe('._isRepeatData', function () {
2692+
describe('from captureMessage', function () {
2693+
beforeEach(function () {
2694+
Raven._lastData = {
2695+
message: 'the thing broke'
2696+
}
2697+
});
2698+
2699+
it('should return true for duplicate captureMessage payloads', function () {
2700+
var data = JSON.parse(JSON.stringify(Raven._lastData)); // copy
2701+
assert.isTrue(Raven._isRepeatData(data));
2702+
});
2703+
2704+
it('should return true for duplicate captureMessage payloads w/ synthetic traces', function () {
2705+
Raven._lastData.stacktrace = {
2706+
frames: [{
2707+
lineno: 100,
2708+
colno: 1337,
2709+
'function': 'lol',
2710+
filename: 'https://example.com/js/foo.js'
2711+
}]
2712+
};
2713+
var data = JSON.parse(JSON.stringify(Raven._lastData)); // copy
2714+
assert.isTrue(Raven._isRepeatData(data));
2715+
});
2716+
2717+
it('should return false for different messages', function () {
2718+
var data = JSON.parse(JSON.stringify(Raven._lastData)); // copy
2719+
data.message = 'the other thing broke';
2720+
2721+
assert.isFalse(Raven._isRepeatData(data));
2722+
});
2723+
2724+
it('should return false for different captureMessage payloads w/ synthetic traces', function () {
2725+
Raven._lastData.stacktrace = {
2726+
frames: [{
2727+
lineno: 100,
2728+
colno: 1337,
2729+
'function': 'lol',
2730+
filename: 'https://example.com/js/foo.js'
2731+
}, {
2732+
lineno: 200,
2733+
colno: 1338,
2734+
'function': 'woo',
2735+
filename: 'https://example.com/js/bar.js'
2736+
}]
2737+
};
2738+
var data = JSON.parse(JSON.stringify(Raven._lastData)); // copy
2739+
data.stacktrace.frames[0].lineno = 101;
2740+
assert.isFalse(Raven._isRepeatData(data));
2741+
2742+
data = JSON.parse(JSON.stringify(Raven._lastData)); // copy
2743+
data.stacktrace.frames.shift(); // different frame count
2744+
assert.isFalse(Raven._isRepeatData(data));
2745+
2746+
data = JSON.parse(JSON.stringify(Raven._lastData)); // copy
2747+
data.stacktrace = undefined; // no stacktrace, same msg
2748+
assert.isFalse(Raven._isRepeatData(data));
2749+
});
2750+
});
2751+
2752+
describe('from captureException/onerror', function () {
2753+
beforeEach(function () {
2754+
Raven._lastData = {
2755+
culprit: 'https://example.com/js/foo.js',
2756+
exception: {
2757+
type: 'TypeError',
2758+
value: 'foo is not defined',
2759+
values: [{
2760+
stacktrace: {
2761+
frames: [{
2762+
lineno: 100,
2763+
colno: 1337,
2764+
'function': 'lol',
2765+
filename: 'https://example.com/js/foo.js'
2766+
}]
2767+
}
2768+
}]
2769+
}
2770+
}
2771+
});
2772+
2773+
it('should return true for duplicate exceptions', function () {
2774+
var data = JSON.parse(JSON.stringify(Raven._lastData)); // copy
2775+
assert.isTrue(Raven._isRepeatData(data));
2776+
});
2777+
2778+
it('should return false for different exceptions', function () {
2779+
var data = JSON.parse(JSON.stringify(Raven._lastData)); // copy
2780+
data.culprit = 'https://example.com/js/bar.js';
2781+
assert.isFalse(Raven._isRepeatData(data));
2782+
2783+
data = JSON.parse(JSON.stringify(Raven._lastData)); // copy
2784+
data.exception.values[0].stacktrace.frames[0].lineno = 101;
2785+
assert.isFalse(Raven._isRepeatData(data));
2786+
2787+
data = JSON.parse(JSON.stringify(Raven._lastData)); // copy
2788+
data.exception.values[0].stacktrace.frames = [];
2789+
assert.isFalse(Raven._isRepeatData(data));
2790+
});
2791+
});
2792+
});
2793+
});
2794+
26782795
// intentionally separate install/uninstall from other test methods, because
26792796
// the built-in wrapping doesn't play nice w/ Sinon's useFakeTimers() [won't
26802797
// restore setTimeout, setInterval, etc]

0 commit comments

Comments
 (0)