Skip to content

Commit cb87941

Browse files
authored
Merge pull request #861 from getsentry/less-dupes
Reject back-to-back duplicate errors/messages
2 parents 4b05748 + 1cfb942 commit cb87941

File tree

7 files changed

+367
-29
lines changed

7 files changed

+367
-29
lines changed

docs/config.rst

+6
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,12 @@ Those configuration options are documented below:
264264
onError
265265
Callback to be invoked upon a failed request.
266266

267+
.. describe:: allowDuplicates
268+
269+
By default, Raven.js attempts to suppress duplicate captured errors and messages that occur back-to-back. Such events are often triggered by rogue code (e.g. from a `setInterval` callback in a browser extension), are not actionable, and eat up your event quota.
270+
271+
To disable this behavior (for example, when testing), set ``allowDuplicates: true`` during configuration.
272+
267273
.. describe:: allowSecretKey
268274

269275
By default, Raven.js will throw an error if configured with a Sentry DSN that contains a secret key.

example/index.html

+2-5
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,9 @@
1515
whitelistUrls: [
1616
/localhost/,
1717
/127\.0\.0\.1/
18-
],
19-
dataCallback: function(data) {
20-
console.log(data);
21-
return data;
22-
}
18+
]
2319
}).install();
20+
Raven.debug = true;
2421

2522
Raven.setUserContext({
2623

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@
3434
"grunt-contrib-uglify": "^0.11.0",
3535
"grunt-eslint": "^17.3.1",
3636
"grunt-gitinfo": "^0.1.7",
37-
"grunt-mocha": "1.0.2",
37+
"grunt-mocha": "1.0.4",
3838
"grunt-release": "^0.13.0",
3939
"grunt-s3": "0.2.0-alpha.3",
4040
"grunt-sri": "mattrobenolt/grunt-sri#pretty",
4141
"jquery": "^2.1.4",
4242
"lodash": "^3.10.1",
43-
"mocha": "^1.21.5",
43+
"mocha": "2.5.3",
4444
"proxyquireify": "^3.0.2",
4545
"sinon": "1.7.3",
4646
"through2": "^2.0.0",

src/raven.js

+93
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ function Raven() {
3131
this._hasDocument = !isUndefined(_document);
3232
this._hasNavigator = !isUndefined(_navigator);
3333
this._lastCapturedException = null;
34+
this._lastData = null;
3435
this._lastEventId = null;
3536
this._globalServer = null;
3637
this._globalKey = null;
@@ -1349,6 +1350,35 @@ Raven.prototype = {
13491350
return this._backoffDuration && now() - this._backoffStart < this._backoffDuration;
13501351
},
13511352

1353+
/**
1354+
* Returns true if the in-process data payload matches the signature
1355+
* of the previously-sent data
1356+
*
1357+
* NOTE: This has to be done at this level because TraceKit can generate
1358+
* data from window.onerror WITHOUT an exception object (IE8, IE9,
1359+
* other old browsers). This can take the form of an "exception"
1360+
* data object with a single frame (derived from the onerror args).
1361+
*/
1362+
_isRepeatData: function (current) {
1363+
var last = this._lastData;
1364+
1365+
if (!last ||
1366+
current.message !== last.message || // defined for captureMessage
1367+
current.culprit !== last.culprit) // defined for captureException/onerror
1368+
return false;
1369+
1370+
// Stacktrace interface (i.e. from captureMessage)
1371+
if (current.stacktrace || last.stacktrace) {
1372+
return isSameStacktrace(current.stacktrace, last.stacktrace);
1373+
}
1374+
// Exception interface (i.e. from captureException/onerror)
1375+
else if (current.exception || last.exception) {
1376+
return isSameException(current.exception, last.exception);
1377+
}
1378+
1379+
return true;
1380+
},
1381+
13521382
_setBackoffState: function(request) {
13531383
// If we are already in a backoff state, don't change anything
13541384
if (this._shouldBackoff()) {
@@ -1475,6 +1505,17 @@ Raven.prototype = {
14751505
// Try and clean up the packet before sending by truncating long values
14761506
data = this._trimPacket(data);
14771507

1508+
// ideally duplicate error testing should occur *before* dataCallback/shouldSendCallback,
1509+
// but this would require copying an un-truncated copy of the data packet, which can be
1510+
// arbitrarily deep (extra_data) -- could be worthwhile? will revisit
1511+
if (!this._globalOptions.allowDuplicates && this._isRepeatData(data)) {
1512+
this._logDebug('warn', 'Raven dropped repeat event: ', data);
1513+
return;
1514+
}
1515+
1516+
// Store outbound payload after trim
1517+
this._lastData = data;
1518+
14781519
this._logDebug('debug', 'Raven about to send:', data);
14791520

14801521
var auth = {
@@ -1836,6 +1877,58 @@ function htmlElementAsString(elem) {
18361877
return out.join('');
18371878
}
18381879

1880+
/**
1881+
* Returns true if either a OR b is truthy, but not both
1882+
*/
1883+
function isOnlyOneTruthy(a, b) {
1884+
return !!(!!a ^ !!b);
1885+
}
1886+
1887+
/**
1888+
* Returns true if the two input exception interfaces have the same content
1889+
*/
1890+
function isSameException(ex1, ex2) {
1891+
if (isOnlyOneTruthy(ex1, ex2))
1892+
return false;
1893+
1894+
ex1 = ex1.values[0];
1895+
ex2 = ex2.values[0];
1896+
1897+
if (ex1.type !== ex2.type ||
1898+
ex1.value !== ex2.value)
1899+
return false;
1900+
1901+
return isSameStacktrace(ex1.stacktrace, ex2.stacktrace);
1902+
}
1903+
1904+
/**
1905+
* Returns true if the two input stack trace interfaces have the same content
1906+
*/
1907+
function isSameStacktrace(stack1, stack2) {
1908+
if (isOnlyOneTruthy(stack1, stack2))
1909+
return false;
1910+
1911+
var frames1 = stack1.frames;
1912+
var frames2 = stack2.frames;
1913+
1914+
// Exit early if frame count differs
1915+
if (frames1.length !== frames2.length)
1916+
return false;
1917+
1918+
// Iterate through every frame; bail out if anything differs
1919+
var a, b;
1920+
for (var i = 0; i < frames1.length; i++) {
1921+
a = frames1[i];
1922+
b = frames2[i];
1923+
if (a.filename !== b.filename ||
1924+
a.lineno !== b.lineno ||
1925+
a.colno !== b.colno ||
1926+
a['function'] !== b['function'])
1927+
return false;
1928+
}
1929+
return true;
1930+
}
1931+
18391932
/**
18401933
* Polyfill a method
18411934
* @param obj object e.g. `document`

test/integration/frame.html

+6
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@
7272
function foo() {
7373
bar();
7474
}
75+
76+
function foo2() {
77+
// identical to foo, but meant for testing
78+
// different stack frame fns w/ same stack length
79+
bar();
80+
}
7581
</script>
7682
</head>
7783
<body>

test/integration/test.js

+118-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ describe('integration', function () {
8585
function () {
8686
setTimeout(done);
8787

88-
8988
Raven.captureException({foo:'bar'});
9089
},
9190
function () {
@@ -120,6 +119,124 @@ describe('integration', function () {
120119
}
121120
);
122121
});
122+
123+
it('should reject duplicate, back-to-back errors from captureError', function (done) {
124+
var iframe = this.iframe;
125+
iframeExecute(iframe, done,
126+
function () {
127+
Raven._breadcrumbs = [];
128+
129+
var count = 5;
130+
setTimeout(function invoke() {
131+
// use setTimeout to capture new error objects that have
132+
// identical stack traces (can't call sequentially or callsite
133+
// line number will change)
134+
//
135+
// order:
136+
// Error: foo
137+
// Error: foo (suppressed)
138+
// Error: foo (suppressed)
139+
// Error: bar
140+
// Error: foo
141+
if (count === 2) {
142+
Raven.captureException(new Error('bar'));
143+
}
144+
else {
145+
Raven.captureException(new Error('foo'));
146+
}
147+
148+
if (count-- === 0) return void done();
149+
else setTimeout(invoke);
150+
});
151+
},
152+
function () {
153+
var breadcrumbs = iframe.contentWindow.Raven._breadcrumbs;
154+
// use breadcrumbs to evaluate which errors were sent
155+
// NOTE: can't use ravenData because duplicate error suppression occurs
156+
// AFTER dataCallback/shouldSendCallback (dataCallback will record
157+
// duplicates but they ultimately won't be sent)
158+
assert.equal(breadcrumbs.length, 3);
159+
assert.equal(breadcrumbs[0].message, 'Error: foo');
160+
assert.equal(breadcrumbs[1].message, 'Error: bar');
161+
assert.equal(breadcrumbs[2].message, 'Error: foo');
162+
}
163+
);
164+
});
165+
166+
it('should not reject back-to-back errors with different stack traces', function (done) {
167+
var iframe = this.iframe;
168+
iframeExecute(iframe, done,
169+
function () {
170+
setTimeout(done);
171+
Raven._breadcrumbs = [];
172+
173+
// same error message, but different stacks means that these are considered
174+
// different errors
175+
// NOTE: PhantomJS can't derive function/lineno/colno from evaled frames, must
176+
// use frames declared in frame.html (foo(), bar())
177+
178+
// stack:
179+
// bar
180+
try {
181+
bar(); // declared in frame.html
182+
} catch (e) {
183+
Raven.captureException(e);
184+
}
185+
186+
// stack (different # frames):
187+
// bar
188+
// foo
189+
try {
190+
foo(); // declared in frame.html
191+
} catch (e) {
192+
Raven.captureException(e);
193+
}
194+
195+
// stack (same # frames, different frames):
196+
// bar
197+
// foo2
198+
try {
199+
foo2(); // declared in frame.html
200+
} catch (e) {
201+
Raven.captureException(e);
202+
}
203+
},
204+
function () {
205+
var breadcrumbs = iframe.contentWindow.Raven._breadcrumbs;
206+
assert.equal(breadcrumbs.length, 3);
207+
// NOTE: regex because exact error message differs per-browser
208+
assert.match(breadcrumbs[0].message, /^ReferenceError.*baz/);
209+
assert.match(breadcrumbs[1].message, /^ReferenceError.*baz/);
210+
assert.match(breadcrumbs[2].message, /^ReferenceError.*baz/);
211+
}
212+
);
213+
});
214+
215+
it('should reject duplicate, back-to-back messages from captureMessage', function (done) {
216+
var iframe = this.iframe;
217+
iframeExecute(iframe, done,
218+
function () {
219+
setTimeout(done);
220+
221+
Raven._breadcrumbs = [];
222+
223+
Raven.captureMessage('this is fine');
224+
Raven.captureMessage('this is fine'); // suppressed
225+
Raven.captureMessage('this is fine', { stacktrace: true });
226+
Raven.captureMessage('i\'m okay with the events that are unfolding currently');
227+
Raven.captureMessage('that\'s okay, things are going to be okay');
228+
},
229+
function () {
230+
var breadcrumbs = iframe.contentWindow.Raven._breadcrumbs;
231+
232+
assert.equal(breadcrumbs.length, 4);
233+
assert.equal(breadcrumbs[0].message, 'this is fine');
234+
assert.equal(breadcrumbs[1].message, 'this is fine'); // with stacktrace
235+
assert.equal(breadcrumbs[2].message, 'i\'m okay with the events that are unfolding currently');
236+
assert.equal(breadcrumbs[3].message, 'that\'s okay, things are going to be okay');
237+
}
238+
);
239+
});
123240
});
124241

125242
describe('window.onerror', function () {

0 commit comments

Comments
 (0)