diff --git a/example/index.html b/example/index.html index b7ebbdd018eb..f6d6823179d4 100644 --- a/example/index.html +++ b/example/index.html @@ -3,8 +3,7 @@ Scratch Disk - - + @@ -14,7 +13,8 @@ //awesome Raven.config('http://50dbe04cd1224d439e9c49bf1d0464df@localhost:8000/1', { whitelistUrls: [ - /localhost/ + /localhost/, + /127\.0\.0\.1/ ], dataCallback: function(data) { console.log(data); @@ -25,7 +25,8 @@ Raven.setUserContext({ email: 'matt@ydekproductions.com', id: 5 -}) +}); + @@ -36,6 +37,7 @@ + diff --git a/example/scratch.js b/example/scratch.js index f1962bbdfc18..9f1fa27dbcd9 100644 --- a/example/scratch.js +++ b/example/scratch.js @@ -40,3 +40,8 @@ function testOptions() { function throwString() { throw 'oops'; } + +function showDialog() { + broken(); + Raven.showReportDialog(); +} diff --git a/src/raven.js b/src/raven.js index cf52cc9c71dd..7cbbdc6e7cc3 100644 --- a/src/raven.js +++ b/src/raven.js @@ -107,6 +107,8 @@ Raven.prototype = { }); } + this._dsn = dsn; + // "Script error." is hard coded into browsers for errors that it can't read. // this is the result of a script being pulled in from an external domain and CORS. this._globalOptions.ignoreErrors.push(/^Script error\.?$/); @@ -121,14 +123,10 @@ Raven.prototype = { this._globalKey = uri.user; this._globalProject = uri.path.substr(lastSlash + 1); - // assemble the endpoint from the uri pieces - this._globalServer = '//' + uri.host + - (uri.port ? ':' + uri.port : '') + - '/' + path + 'api/' + this._globalProject + '/store/'; + this._globalServer = this._getGlobalServer(uri); - if (uri.protocol) { - this._globalServer = uri.protocol + ':' + this._globalServer; - } + this._globalEndpoint = this._globalServer + + '/' + path + 'api/' + this._globalProject + '/store/'; if (this._globalOptions.fetchContext) { TraceKit.remoteFetching = true; @@ -498,6 +496,41 @@ Raven.prototype = { } }, + showReportDialog: function (options) { + if (!window.document) // doesn't work without a document (React native) + return; + + options = options || {}; + + var lastEventId = options.eventId || this.lastEventId(); + if (!lastEventId) { + throw new RavenConfigError('Missing eventId'); + } + + var dsn = options.dsn || this._dsn; + if (!dsn) { + throw new RavenConfigError('Missing DSN'); + } + + var encode = encodeURIComponent; + var qs = ''; + qs += '?eventId=' + encode(lastEventId); + qs += '&dsn=' + encode(dsn); + + var user = options.user || this._globalContext.user; + if (user) { + if (user.name) qs += '&name=' + encode(user.name); + if (user.email) qs += '&email=' + encode(user.email); + } + + var globalServer = this._getGlobalServer(this._parseDSN(dsn)); + + var script = document.createElement('script'); + script.async = true; + script.src = globalServer + '/api/embed/error-page/' + qs; + (document.head || document.body).appendChild(script); + }, + /**** Private functions ****/ _ignoreNextOnError: function () { var self = this; @@ -683,6 +716,17 @@ Raven.prototype = { return dsn; }, + _getGlobalServer: function(uri) { + // assemble the endpoint from the uri pieces + var globalServer = '//' + uri.host + + (uri.port ? ':' + uri.port : ''); + + if (uri.protocol) { + globalServer = uri.protocol + ':' + globalServer; + } + return globalServer; + }, + _handleOnErrorStackInfo: function() { // if we are intentionally ignoring errors via onerror, bail out if (!this._ignoreOnError) { @@ -933,8 +977,9 @@ Raven.prototype = { if (!this.isSetup()) return; + var url = this._globalEndpoint; (globalOptions.transport || this._makeRequest).call(this, { - url: this._globalServer, + url: url, auth: { sentry_version: '7', sentry_client: 'raven-js/' + this.VERSION, @@ -945,13 +990,13 @@ Raven.prototype = { onSuccess: function success() { self._triggerEvent('success', { data: data, - src: self._globalServer + src: url }); }, onError: function failure() { self._triggerEvent('failure', { data: data, - src: self._globalServer + src: url }); } }); diff --git a/test/raven.test.js b/test/raven.test.js index 64c605786be2..ba2bf7036771 100644 --- a/test/raven.test.js +++ b/test/raven.test.js @@ -5,6 +5,7 @@ var proxyquire = require('proxyquireify')(require); var TraceKit = require('../vendor/TraceKit/tracekit'); + var _Raven = proxyquire('../src/raven', { './utils': { // patched to return a predictable result @@ -1013,7 +1014,7 @@ describe('globals', function() { maxMessageLength: 100, release: 'abc123', }; - Raven._globalServer = 'http://localhost/store/'; + Raven._globalEndpoint = 'http://localhost/store/'; Raven._globalOptions = globalOptions; Raven._send({message: 'bar'}); @@ -1226,7 +1227,7 @@ describe('globals', function() { it('should populate crossOrigin based on options', function() { Raven._makeImageRequest({ - url: Raven._globalServer, + url: Raven._globalEndpoint, auth: {lol: '1'}, data: {foo: 'bar'}, options: { @@ -1239,7 +1240,7 @@ describe('globals', function() { it('should populate crossOrigin if empty string', function() { Raven._makeImageRequest({ - url: Raven._globalServer, + url: Raven._globalEndpoint, auth: {lol: '1'}, data: {foo: 'bar'}, options: { @@ -1252,7 +1253,7 @@ describe('globals', function() { it('should not populate crossOrigin if falsey', function() { Raven._makeImageRequest({ - url: Raven._globalServer, + url: Raven._globalEndpoint, auth: {lol: '1'}, data: {foo: 'bar'}, options: { @@ -1487,7 +1488,7 @@ describe('Raven (public API)', function() { Raven.afterLoad(); assert.equal(Raven._globalKey, 'random'); - assert.equal(Raven._globalServer, 'http://some.other.server:80/api/2/store/'); + assert.equal(Raven._globalEndpoint, 'http://some.other.server:80/api/2/store/'); assert.equal(Raven._globalOptions.some, 'config'); assert.equal(Raven._globalProject, '2'); @@ -1504,7 +1505,7 @@ describe('Raven (public API)', function() { assert.equal(Raven, Raven.config(SENTRY_DSN, {foo: 'bar'}), 'it should return Raven'); assert.equal(Raven._globalKey, 'abc'); - assert.equal(Raven._globalServer, 'http://example.com:80/api/2/store/'); + assert.equal(Raven._globalEndpoint, 'http://example.com:80/api/2/store/'); assert.equal(Raven._globalOptions.foo, 'bar'); assert.equal(Raven._globalProject, '2'); assert.isTrue(Raven.isSetup()); @@ -1514,7 +1515,7 @@ describe('Raven (public API)', function() { Raven.config('//abc@example.com/2'); assert.equal(Raven._globalKey, 'abc'); - assert.equal(Raven._globalServer, '//example.com/api/2/store/'); + assert.equal(Raven._globalEndpoint, '//example.com/api/2/store/'); assert.equal(Raven._globalProject, '2'); assert.isTrue(Raven.isSetup()); }); @@ -1522,7 +1523,7 @@ describe('Raven (public API)', function() { it('should work should work at a non root path', function() { Raven.config('//abc@example.com/sentry/2'); assert.equal(Raven._globalKey, 'abc'); - assert.equal(Raven._globalServer, '//example.com/sentry/api/2/store/'); + assert.equal(Raven._globalEndpoint, '//example.com/sentry/api/2/store/'); assert.equal(Raven._globalProject, '2'); assert.isTrue(Raven.isSetup()); }); @@ -2028,4 +2029,80 @@ describe('Raven (public API)', function() { assert.isFalse(Raven.isSetup()); }); }); + + describe('.showReportDialog', function () { + it('should throw a RavenConfigError if no eventId', function () { + assert.throws(function () { + Raven.showReportDialog({ + dsn: SENTRY_DSN // dsn specified via options + }); + }, 'Missing eventId'); + + Raven.config(SENTRY_DSN); + assert.throws(function () { + Raven.showReportDialog(); // dsn specified via Raven.config + }, 'Missing eventId'); + }); + + it('should throw a RavenConfigError if no dsn', function () { + assert.throws(function () { + Raven.showReportDialog({ + eventId: 'abc123' + }); + }, 'Missing DSN'); + }); + + describe('script tag insertion', function () { + beforeEach(function () { + this.appendChildStub = this.sinon.stub(document.head, 'appendChild'); + }); + + it('should specify embed API endpoint and basic query string (DSN, eventId)', function () { + Raven.showReportDialog({ + eventId: 'abc123', + dsn: SENTRY_DSN + }); + + var script = this.appendChildStub.getCall(0).args[0]; + assert.equal(script.src, 'http://example.com/api/embed/error-page/?eventId=abc123&dsn=http%3A%2F%2Fabc%40example.com%3A80%2F2'); + + this.appendChildStub.reset(); + + Raven + .config(SENTRY_DSN) + .captureException(new Error('foo')) // generates lastEventId + .showReportDialog(); + + this.appendChildStub.getCall(0).args[0]; + assert.equal(script.src, 'http://example.com/api/embed/error-page/?eventId=abc123&dsn=http%3A%2F%2Fabc%40example.com%3A80%2F2'); + }); + + it('should specify embed API endpoint and full query string (DSN, eventId, user)', function () { + Raven.showReportDialog({ + eventId: 'abc123', + dsn: SENTRY_DSN, + user: { + name: 'Average Normalperson', + email: 'an@example.com' + } + }); + + var script = this.appendChildStub.getCall(0).args[0]; + assert.equal(script.src, 'http://example.com/api/embed/error-page/?eventId=abc123&dsn=http%3A%2F%2Fabc%40example.com%3A80%2F2&name=Average%20Normalperson&email=an%40example.com'); + + this.appendChildStub.reset(); + Raven + .config(SENTRY_DSN) + .captureException(new Error('foo')) // generates lastEventId + .setUserContext({ + name: 'Average Normalperson 2', + email: 'an2@example.com' + }) + .showReportDialog(); + + var script = this.appendChildStub.getCall(0).args[0]; + assert.equal(script.src, 'http://example.com/api/embed/error-page/?eventId=abc123&dsn=http%3A%2F%2Fabc%40example.com%3A80%2F2&name=Average%20Normalperson%202&email=an2%40example.com'); + }); + }); + }); });