diff --git a/src/raven.js b/src/raven.js index 77964b91b732..0f1d5d21f2be 100644 --- a/src/raven.js +++ b/src/raven.js @@ -1,3 +1,4 @@ +/*global XDomainRequest:false*/ 'use strict'; // First, check for JSON support @@ -105,7 +106,9 @@ var Raven = { (uri.port ? ':' + uri.port : '') + '/' + path + 'api/' + globalProject + '/store/'; - if (uri.protocol) { + // can safely use protocol relative (//) if target host is + // app.getsentry.com; otherwise use protocol from DSN + if (uri.protocol && uri.host !== 'app.getsentry.com') { globalServer = uri.protocol + ':' + globalServer; } @@ -840,7 +843,7 @@ function send(data) { }); } -function makeRequest(opts) { +function makeImageRequest(opts) { // Tack on sentry_data to auth options, which get urlencoded opts.auth.sentry_data = JSON.stringify(opts.data); @@ -856,6 +859,47 @@ function makeRequest(opts) { img.src = src; } +function makeXhrRequest(opts) { + var request; + + function handler() { + if (request.status === 200) { + if (opts.onSuccess) { + opts.onSuccess(); + } + } else if (opts.onError) { + opts.onError(); + } + } + + request = new XMLHttpRequest(); + if ('withCredentials' in request) { + request.onreadystatechange = function () { + if (request.readyState !== 4) { + return; + } + handler(); + }; + } else { + request = new XDomainRequest(); + // onreadystatechange not supported by XDomainRequest + request.onload = handler; + } + + // NOTE: auth is intentionally sent as part of query string (NOT as custom + // HTTP header) so as to avoid preflight CORS requests + request.open('POST', opts.url + '?' + urlencode(opts.auth)); + request.send(JSON.stringify(opts.data)); +} + +function makeRequest(opts) { + var hasCORS = + 'withCredentials' in new XMLHttpRequest() || + typeof XDomainRequest !== 'undefined'; + + return (hasCORS ? makeXhrRequest : makeImageRequest)(opts); +} + // Note: this is shitty, but I can't figure out how to get // sinon to stub document.createElement without breaking everything // so this wrapper is just so I can stub it for tests. diff --git a/test/raven.test.js b/test/raven.test.js index 5cc46898f7cf..4162386b0d90 100644 --- a/test/raven.test.js +++ b/test/raven.test.js @@ -1338,6 +1338,84 @@ describe('globals', function() { }); describe('makeRequest', function() { + beforeEach(function() { + // use fake xml http request so we can muck w/ its prototype + this.xhr = sinon.useFakeXMLHttpRequest(); + this.sinon.stub(window, 'makeImageRequest'); + this.sinon.stub(window, 'makeXhrRequest'); + }); + + afterEach(function() { + this.xhr.restore(); + }); + + it('should call makeXhrRequest if CORS is supported', function () { + XMLHttpRequest.prototype.withCredentials = true; + + makeRequest({ + url: 'http://localhost/', + auth: {a: '1', b: '2'}, + data: {foo: 'bar'}, + options: globalOptions + }); + + assert.isTrue(makeImageRequest.notCalled); + assert.isTrue(makeXhrRequest.calledOnce); + }); + + it('should call makeImageRequest if CORS is NOT supported', function () { + delete XMLHttpRequest.prototype.withCredentials; + + var oldXDR = window.XDomainRequest; + window.XDomainRequest = undefined; + + makeRequest({ + url: 'http://localhost/', + auth: {a: '1', b: '2'}, + data: {foo: 'bar'}, + options: globalOptions + }); + + assert.isTrue(makeImageRequest.calledOnce); + assert.isTrue(makeXhrRequest.notCalled); + + window.XDomainRequest = oldXDR; + }); + }); + + describe('makeXhrRequest', function() { + beforeEach(function() { + // NOTE: can't seem to call useFakeXMLHttpRequest via sandbox; must + // restore manually + this.xhr = sinon.useFakeXMLHttpRequest(); + var requests = this.requests = []; + + this.xhr.onCreate = function (xhr) { + requests.push(xhr); + }; + }); + + afterEach(function() { + this.xhr.restore(); + }); + + it('should create an XMLHttpRequest object with body as JSON payload', function() { + XMLHttpRequest.prototype.withCredentials = true; + + makeXhrRequest({ + url: 'http://localhost/', + auth: {a: '1', b: '2'}, + data: {foo: 'bar'}, + options: globalOptions + }); + + var lastXhr = this.requests[this.requests.length - 1]; + assert.equal(lastXhr.requestBody, '{"foo":"bar"}'); + assert.equal(lastXhr.url, 'http://localhost/?a=1&b=2'); + }); + }); + + describe('makeImageRequest', function() { var imageCache; beforeEach(function () { @@ -1346,7 +1424,7 @@ describe('globals', function() { }) it('should load an Image', function() { - makeRequest({ + makeImageRequest({ url: 'http://localhost/', auth: {a: '1', b: '2'}, data: {foo: 'bar'}, @@ -1360,7 +1438,7 @@ describe('globals', function() { globalOptions = { crossOrigin: 'something', }; - makeRequest({ + makeImageRequest({ url: globalServer, auth: {lol: '1'}, data: {foo: 'bar'}, @@ -1374,7 +1452,7 @@ describe('globals', function() { globalOptions = { crossOrigin: '' }; - makeRequest({ + makeImageRequest({ url: globalServer, auth: {lol: '1'}, data: {foo: 'bar'}, @@ -1388,7 +1466,7 @@ describe('globals', function() { globalOptions = { crossOrigin: false }; - makeRequest({ + makeImageRequest({ url: globalServer, auth: {lol: '1'}, data: {foo: 'bar'}, @@ -2043,11 +2121,11 @@ describe('Raven (public API)', function() { it('should work as advertised #integration', function() { var imageCache = []; - this.sinon.stub(window, 'newImage', function(){ var img = {}; imageCache.push(img); return img; }); + this.sinon.stub(window, 'makeRequest'); setupRaven(); Raven.captureMessage('lol', {foo: 'bar'}); - assert.equal(imageCache.length, 1); + assert.equal(window.makeRequest.callCount, 1); // It'd be hard to assert the actual payload being sent // since it includes the generated url, which is going to // vary between users running the tests