Skip to content

Commit 9f4a6f6

Browse files
committed
feat: Use Fetch instead of XHR when available
1 parent 9894ae1 commit 9f4a6f6

File tree

3 files changed

+207
-63
lines changed

3 files changed

+207
-63
lines changed

src/raven.js

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var isSameException = utils.isSameException;
2626
var isSameStacktrace = utils.isSameStacktrace;
2727
var parseUrl = utils.parseUrl;
2828
var fill = utils.fill;
29+
var supportsFetch = utils.supportsFetch;
2930

3031
var wrapConsoleMethod = require('./console').wrapMethod;
3132

@@ -1179,7 +1180,7 @@ Raven.prototype = {
11791180
xhrproto,
11801181
'send',
11811182
function(origSend) {
1182-
return function(data) {
1183+
return function() {
11831184
// preserve arity
11841185
var xhr = this;
11851186

@@ -1227,12 +1228,12 @@ Raven.prototype = {
12271228
);
12281229
}
12291230

1230-
if (autoBreadcrumbs.xhr && 'fetch' in _window) {
1231+
if (autoBreadcrumbs.xhr && supportsFetch()) {
12311232
fill(
12321233
_window,
12331234
'fetch',
12341235
function(origFetch) {
1235-
return function(fn, t) {
1236+
return function() {
12361237
// preserve arity
12371238
// Make a copy of the arguments to prevent deoptimization
12381239
// https://github.com/petkaantonov/bluebird/wiki/Optimization-killers#32-leaking-arguments
@@ -1256,6 +1257,11 @@ Raven.prototype = {
12561257
url = '' + fetchInput;
12571258
}
12581259

1260+
// if Sentry key appears in URL, don't capture, as it's our own request
1261+
if (url.indexOf(self._globalKey) !== -1) {
1262+
return origFetch.apply(this, args);
1263+
}
1264+
12591265
if (args[1] && args[1].method) {
12601266
method = args[1].method;
12611267
}
@@ -1692,8 +1698,14 @@ Raven.prototype = {
16921698
try {
16931699
// If Retry-After is not in Access-Control-Expose-Headers, most
16941700
// browsers will throw an exception trying to access it
1695-
retry = request.getResponseHeader('Retry-After');
1696-
retry = parseInt(retry, 10) * 1000; // Retry-After is returned in seconds
1701+
if (supportsFetch()) {
1702+
retry = request.headers.get('Retry-After');
1703+
} else {
1704+
retry = request.getResponseHeader('Retry-After');
1705+
}
1706+
1707+
// Retry-After is returned in seconds
1708+
retry = parseInt(retry, 10) * 1000;
16971709
} catch (e) {
16981710
/* eslint no-empty:0 */
16991711
}
@@ -1882,6 +1894,32 @@ Raven.prototype = {
18821894
},
18831895

18841896
_makeRequest: function(opts) {
1897+
// Auth is intentionally sent as part of query string (NOT as custom HTTP header) to avoid preflight CORS requests
1898+
var url = opts.url + '?' + urlencode(opts.auth);
1899+
1900+
if (supportsFetch()) {
1901+
return _window
1902+
.fetch(url, {
1903+
method: 'POST',
1904+
body: stringify(opts.data)
1905+
})
1906+
.then(function(response) {
1907+
if (response.ok) {
1908+
opts.onSuccess && opts.onSuccess();
1909+
} else {
1910+
var error = new Error('Sentry error code: ' + response.status);
1911+
// It's called request only to keep compatibility with XHR interface
1912+
// and not add more redundant checks in setBackoffState method
1913+
error.request = response;
1914+
opts.onError && opts.onError(error);
1915+
}
1916+
})
1917+
['catch'](function() {
1918+
opts.onError &&
1919+
opts.onError(new Error('Sentry error code: network unavailable'));
1920+
});
1921+
}
1922+
18851923
var request = _window.XMLHttpRequest && new _window.XMLHttpRequest();
18861924
if (!request) return;
18871925

@@ -1890,8 +1928,6 @@ Raven.prototype = {
18901928

18911929
if (!hasCORS) return;
18921930

1893-
var url = opts.url;
1894-
18951931
if ('withCredentials' in request) {
18961932
request.onreadystatechange = function() {
18971933
if (request.readyState !== 4) {
@@ -1923,9 +1959,7 @@ Raven.prototype = {
19231959
}
19241960
}
19251961

1926-
// NOTE: auth is intentionally sent as part of query string (NOT as custom
1927-
// HTTP header) so as to avoid preflight CORS requests
1928-
request.open('POST', url + '?' + urlencode(opts.auth));
1962+
request.open('POST', url);
19291963
request.send(stringify(opts.data));
19301964
},
19311965

src/utils.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ function supportsErrorEvent() {
6060
}
6161
}
6262

63+
function supportsFetch() {
64+
if (!('fetch' in _window)) return false;
65+
66+
try {
67+
new Headers(); // eslint-disable-line no-new
68+
new Request(''); // eslint-disable-line no-new
69+
new Response(); // eslint-disable-line no-new
70+
return true;
71+
} catch (e) {
72+
return false;
73+
}
74+
}
75+
6376
function wrappedCallback(callback) {
6477
function dataCallback(data, original) {
6578
var normalizedData = callback(data) || data;
@@ -378,6 +391,7 @@ module.exports = {
378391
isArray: isArray,
379392
isEmptyObject: isEmptyObject,
380393
supportsErrorEvent: supportsErrorEvent,
394+
supportsFetch: supportsFetch,
381395
wrappedCallback: wrappedCallback,
382396
each: each,
383397
objectMerge: objectMerge,

test/raven.test.js

Lines changed: 149 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ _Raven.prototype._getUuid = function() {
2323
var utils = require('../src/utils');
2424
var joinRegExp = utils.joinRegExp;
2525
var supportsErrorEvent = utils.supportsErrorEvent;
26+
var supportsFetch = utils.supportsFetch;
2627

2728
// window.console must be stubbed in for browsers that don't have it
2829
if (typeof window.console === 'undefined') {
@@ -1495,7 +1496,10 @@ describe('globals', function() {
14951496
assert.equal(Raven._backoffDuration, 2000);
14961497
});
14971498

1498-
it('should set backoffDuration to value of Retry-If header if present', function() {
1499+
it('should set backoffDuration to value of Retry-If header if present - XHR API', function() {
1500+
var origFetch = window.fetch;
1501+
delete window.fetch;
1502+
14991503
this.sinon.stub(Raven, 'isSetup').returns(true);
15001504
this.sinon.stub(Raven, '_makeRequest');
15011505

@@ -1509,12 +1513,40 @@ describe('globals', function() {
15091513
.withArgs('Retry-After')
15101514
.returns('2')
15111515
};
1516+
15121517
opts.onError(mockError);
15131518

15141519
assert.equal(Raven._backoffStart, 100); // clock is at 100ms
15151520
assert.equal(Raven._backoffDuration, 2000); // converted to ms, int
1521+
1522+
window.fetch = origFetch;
15161523
});
15171524

1525+
if (supportsFetch()) {
1526+
it('should set backoffDuration to value of Retry-If header if present - FETCH API', function() {
1527+
this.sinon.stub(Raven, 'isSetup').returns(true);
1528+
this.sinon.stub(Raven, '_makeRequest');
1529+
1530+
Raven._send({message: 'bar'});
1531+
var opts = Raven._makeRequest.lastCall.args[0];
1532+
var mockError = new Error('401: Unauthorized');
1533+
mockError.request = {
1534+
status: 401,
1535+
headers: {
1536+
get: sinon
1537+
.stub()
1538+
.withArgs('Retry-After')
1539+
.returns('2')
1540+
}
1541+
};
1542+
1543+
opts.onError(mockError);
1544+
1545+
assert.equal(Raven._backoffStart, 100); // clock is at 100ms
1546+
assert.equal(Raven._backoffDuration, 2000); // converted to ms, int
1547+
});
1548+
}
1549+
15181550
it('should reset backoffDuration and backoffStart if onSuccess is fired (200)', function() {
15191551
this.sinon.stub(Raven, 'isSetup').returns(true);
15201552
this.sinon.stub(Raven, '_makeRequest');
@@ -1653,74 +1685,138 @@ describe('globals', function() {
16531685
});
16541686

16551687
describe('makeRequest', function() {
1656-
beforeEach(function() {
1657-
// NOTE: can't seem to call useFakeXMLHttpRequest via sandbox; must
1658-
// restore manually
1659-
this.xhr = sinon.useFakeXMLHttpRequest();
1660-
var requests = (this.requests = []);
1688+
if (supportsFetch()) {
1689+
describe('using Fetch API', function() {
1690+
afterEach(function() {
1691+
window.fetch.restore();
1692+
});
16611693

1662-
this.xhr.onCreate = function(xhr) {
1663-
requests.push(xhr);
1664-
};
1665-
});
1694+
it('should create an XMLHttpRequest object with body as JSON payload', function() {
1695+
this.sinon.spy(window, 'fetch');
16661696

1667-
afterEach(function() {
1668-
this.xhr.restore();
1669-
});
1697+
Raven._makeRequest({
1698+
url: 'http://localhost/',
1699+
auth: {a: '1', b: '2'},
1700+
data: {foo: 'bar'},
1701+
options: Raven._globalOptions
1702+
});
1703+
1704+
assert.deepEqual(window.fetch.lastCall.args, [
1705+
'http://localhost/?a=1&b=2',
1706+
{
1707+
method: 'POST',
1708+
body: '{"foo":"bar"}'
1709+
}
1710+
]);
1711+
});
16701712

1671-
it('should create an XMLHttpRequest object with body as JSON payload', function() {
1672-
XMLHttpRequest.prototype.withCredentials = true;
1713+
it('should pass a request object to onError', function(done) {
1714+
sinon.stub(window, 'fetch');
1715+
window.fetch.returns(
1716+
Promise.resolve(
1717+
new window.Response('{"foo":"bar"}', {
1718+
ok: false,
1719+
status: 429,
1720+
headers: {
1721+
'Content-type': 'text/html'
1722+
}
1723+
})
1724+
)
1725+
);
1726+
1727+
Raven._makeRequest({
1728+
url: 'http://localhost/',
1729+
auth: {a: '1', b: '2'},
1730+
data: {foo: 'bar'},
1731+
options: Raven._globalOptions,
1732+
onError: function(error) {
1733+
assert.equal(error.request.status, 429);
1734+
done();
1735+
}
1736+
});
1737+
});
1738+
});
1739+
}
1740+
1741+
describe('using XHR API', function() {
1742+
var origFetch = window.fetch;
1743+
var xhr;
1744+
var requests;
16731745

1674-
Raven._makeRequest({
1675-
url: 'http://localhost/',
1676-
auth: {a: '1', b: '2'},
1677-
data: {foo: 'bar'},
1678-
options: Raven._globalOptions
1746+
before(function() {
1747+
delete window.fetch;
16791748
});
16801749

1681-
var lastXhr = this.requests[this.requests.length - 1];
1682-
assert.equal(lastXhr.requestBody, '{"foo":"bar"}');
1683-
assert.equal(lastXhr.url, 'http://localhost/?a=1&b=2');
1684-
});
1750+
after(function() {
1751+
window.fetch = origFetch;
1752+
});
16851753

1686-
it('should no-op if CORS is not supported', function() {
1687-
delete XMLHttpRequest.prototype.withCredentials;
1688-
var oldSupportsCORS = sinon.xhr.supportsCORS;
1689-
sinon.xhr.supportsCORS = false;
1754+
beforeEach(function() {
1755+
// NOTE: can't seem to call useFakeXMLHttpRequest via sandbox; must restore manually
1756+
xhr = sinon.useFakeXMLHttpRequest();
1757+
requests = [];
16901758

1691-
var oldXDR = window.XDomainRequest;
1692-
window.XDomainRequest = undefined;
1759+
XMLHttpRequest.prototype.withCredentials = true;
16931760

1694-
Raven._makeRequest({
1695-
url: 'http://localhost/',
1696-
auth: {a: '1', b: '2'},
1697-
data: {foo: 'bar'},
1698-
options: Raven._globalOptions
1761+
xhr.onCreate = function(xhr) {
1762+
requests.push(xhr);
1763+
};
16991764
});
17001765

1701-
assert.equal(this.requests.length, 1); // the "test" xhr
1702-
assert.equal(this.requests[0].readyState, 0);
1766+
afterEach(function() {
1767+
xhr.restore();
1768+
});
17031769

1704-
sinon.xhr.supportsCORS = oldSupportsCORS;
1705-
window.XDomainRequest = oldXDR;
1706-
});
1770+
it('should create an XMLHttpRequest object with body as JSON payload', function() {
1771+
Raven._makeRequest({
1772+
url: 'http://localhost/',
1773+
auth: {a: '1', b: '2'},
1774+
data: {foo: 'bar'},
1775+
options: Raven._globalOptions
1776+
});
17071777

1708-
it('should pass a request object to onError', function(done) {
1709-
XMLHttpRequest.prototype.withCredentials = true;
1778+
var lastXhr = requests[requests.length - 1];
1779+
assert.equal(lastXhr.requestBody, '{"foo":"bar"}');
1780+
assert.equal(lastXhr.url, 'http://localhost/?a=1&b=2');
1781+
});
17101782

1711-
Raven._makeRequest({
1712-
url: 'http://localhost/',
1713-
auth: {a: '1', b: '2'},
1714-
data: {foo: 'bar'},
1715-
options: Raven._globalOptions,
1716-
onError: function(error) {
1717-
assert.equal(error.request.status, 429);
1718-
done();
1719-
}
1783+
it('should pass a request object to onError', function(done) {
1784+
Raven._makeRequest({
1785+
url: 'http://localhost/',
1786+
auth: {a: '1', b: '2'},
1787+
data: {foo: 'bar'},
1788+
options: Raven._globalOptions,
1789+
onError: function(error) {
1790+
assert.equal(error.request.status, 429);
1791+
done();
1792+
}
1793+
});
1794+
1795+
var lastXhr = requests[requests.length - 1];
1796+
lastXhr.respond(429, {'Content-Type': 'text/html'}, 'Too many requests');
17201797
});
17211798

1722-
var lastXhr = this.requests[this.requests.length - 1];
1723-
lastXhr.respond(429, {'Content-Type': 'text/html'}, 'Too many requests');
1799+
it('should no-op if CORS is not supported', function() {
1800+
delete XMLHttpRequest.prototype.withCredentials;
1801+
var oldSupportsCORS = sinon.xhr.supportsCORS;
1802+
sinon.xhr.supportsCORS = false;
1803+
1804+
var oldXDR = window.XDomainRequest;
1805+
window.XDomainRequest = undefined;
1806+
1807+
Raven._makeRequest({
1808+
url: 'http://localhost/',
1809+
auth: {a: '1', b: '2'},
1810+
data: {foo: 'bar'},
1811+
options: Raven._globalOptions
1812+
});
1813+
1814+
assert.equal(requests.length, 1); // the "test" xhr
1815+
assert.equal(requests[0].readyState, 0);
1816+
1817+
sinon.xhr.supportsCORS = oldSupportsCORS;
1818+
window.XDomainRequest = oldXDR;
1819+
});
17241820
});
17251821
});
17261822

0 commit comments

Comments
 (0)