Skip to content

Commit bfba649

Browse files
committed
Exponential backoff if Sentry server returns 429
1 parent d51bdba commit bfba649

File tree

2 files changed

+110
-1
lines changed

2 files changed

+110
-1
lines changed

src/raven.js

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ function Raven() {
6161
this._keypressTimeout;
6262
this._location = _window.location;
6363
this._lastHref = this._location && this._location.href;
64+
this._backoffDuration = 0;
65+
this._backoffStart = null;
6466

6567
for (var method in this._originalConsole) { // eslint-disable-line guard-for-in
6668
this._originalConsoleMethods[method] = this._originalConsole[method];
@@ -1320,8 +1322,15 @@ Raven.prototype = {
13201322
return httpData;
13211323
},
13221324

1325+
_shouldBackoff: function() {
1326+
return this._backoffDuration && now() - this._backoffStart < this._backoffDuration;
1327+
},
13231328

13241329
_send: function(data) {
1330+
if (this._shouldBackoff()) {
1331+
return;
1332+
}
1333+
13251334
var globalOptions = this._globalOptions;
13261335

13271336
var baseData = {
@@ -1434,13 +1443,22 @@ Raven.prototype = {
14341443
data: data,
14351444
options: globalOptions,
14361445
onSuccess: function success() {
1446+
self._backoffDuration = 0;
1447+
self._backoffStart = null;
1448+
14371449
self._triggerEvent('success', {
14381450
data: data,
14391451
src: url
14401452
});
14411453
callback && callback();
14421454
},
14431455
onError: function failure(error) {
1456+
// too many requests
1457+
if (!self._shouldBackoff() && error.request && error.request.status === 429) {
1458+
self._backoffDuration = self._backoffDuration * 2 || 1000;
1459+
self._backoffStart = now();
1460+
}
1461+
14441462
self._triggerEvent('failure', {
14451463
data: data,
14461464
src: url
@@ -1468,7 +1486,9 @@ Raven.prototype = {
14681486
opts.onSuccess();
14691487
}
14701488
} else if (opts.onError) {
1471-
opts.onError(new Error('Sentry error code: ' + request.status));
1489+
var err = new Error('Sentry error code: ' + request.status);
1490+
err.request = request;
1491+
opts.onError(err);
14721492
}
14731493
}
14741494

test/raven.test.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,77 @@ describe('globals', function() {
11371137
assert.equal(data.message, shortMessage);
11381138
assert.equal(data.exception.values[0].value, shortMessage);
11391139
});
1140+
1141+
it('should bail out if time elapsed does not exceed backoffDuration', function () {
1142+
this.sinon.stub(Raven, 'isSetup').returns(true);
1143+
this.sinon.stub(Raven, '_makeRequest');
1144+
1145+
Raven._backoffDuration = 1000;
1146+
Raven._backoffStart = 100;
1147+
this.clock.tick(100); // tick 100 ms - NOT past backoff duration
1148+
1149+
Raven._send({message: 'bar'});
1150+
assert.isFalse(Raven._makeRequest.called);
1151+
});
1152+
1153+
it('should proceed if time elapsed exceeds backoffDuration', function () {
1154+
this.sinon.stub(Raven, 'isSetup').returns(true);
1155+
this.sinon.stub(Raven, '_makeRequest');
1156+
1157+
Raven._backoffDuration = 1000;
1158+
Raven._backoffStart = 100;
1159+
this.clock.tick(1000); // advance clock 1000 ms - past backoff duration
1160+
1161+
Raven._send({message: 'bar'});
1162+
assert.isTrue(Raven._makeRequest.called);
1163+
});
1164+
1165+
it('should set backoffDuration and backoffStart if onError is fired w/ 429 response', function () {
1166+
this.sinon.stub(Raven, 'isSetup').returns(true);
1167+
this.sinon.stub(Raven, '_makeRequest');
1168+
1169+
Raven._send({message: 'bar'});
1170+
var opts = Raven._makeRequest.lastCall.args[0];
1171+
var mockError = new Error('429: Too many requests');
1172+
mockError.request = {
1173+
status: 429
1174+
};
1175+
opts.onError(mockError);
1176+
1177+
assert.equal(Raven._backoffStart, 100); // clock is at 100ms
1178+
assert.equal(Raven._backoffDuration, 1000);
1179+
1180+
this.clock.tick(1); // only 1ms
1181+
opts.onError(mockError);
1182+
1183+
// since the backoff has started, a subsequent 429 within the backoff period
1184+
// should not not the start/duration
1185+
assert.equal(Raven._backoffStart, 100);
1186+
assert.equal(Raven._backoffDuration, 1000);
1187+
1188+
this.clock.tick(1000); // move past backoff period
1189+
opts.onError(mockError);
1190+
1191+
// another failure has occurred, this time *after* the backoff period - should increase
1192+
assert.equal(Raven._backoffStart, 1101);
1193+
assert.equal(Raven._backoffDuration, 2000);
1194+
});
1195+
1196+
it('should reset backoffDuration and backoffStart if onSuccess is fired (200)', function () {
1197+
this.sinon.stub(Raven, 'isSetup').returns(true);
1198+
this.sinon.stub(Raven, '_makeRequest');
1199+
1200+
Raven._backoffDuration = 1000;
1201+
Raven._backoffStart = 0;
1202+
this.clock.tick(1001); // tick clock just past time necessary
1203+
1204+
Raven._send({message: 'bar'});
1205+
var opts = Raven._makeRequest.lastCall.args[0];
1206+
opts.onSuccess({});
1207+
1208+
assert.equal(Raven._backoffStart, null); // clock is at 100ms
1209+
assert.equal(Raven._backoffDuration, 0);
1210+
});
11401211
});
11411212

11421213
describe('makeRequest', function() {
@@ -1188,6 +1259,24 @@ describe('globals', function() {
11881259

11891260
window.XDomainRequest = oldXDR
11901261
});
1262+
1263+
it('should pass a request object to onError', function (done) {
1264+
XMLHttpRequest.prototype.withCredentials = true;
1265+
1266+
Raven._makeRequest({
1267+
url: 'http://localhost/',
1268+
auth: {a: '1', b: '2'},
1269+
data: {foo: 'bar'},
1270+
options: Raven._globalOptions,
1271+
onError: function (error) {
1272+
assert.equal(error.request.status, 429);
1273+
done();
1274+
}
1275+
});
1276+
1277+
var lastXhr = this.requests[this.requests.length - 1];
1278+
lastXhr.respond(429, {'Content-Type': 'text/html'}, 'Too many requests');
1279+
});
11911280
});
11921281

11931282
describe('handleOnErrorStackInfo', function () {

0 commit comments

Comments
 (0)