diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst
index 324e7752894c..ee717604f148 100644
--- a/docs/plugins/index.rst
+++ b/docs/plugins/index.rst
@@ -13,6 +13,57 @@ Why are plugins needed at all?
JavaScript is pretty restrictive when it comes to exception handling, and there are a lot of things that make it difficult to get relevent information, so it's important that we inject code and wrap things magically so we can extract what we need. See :doc:`/usage/index` for tips regarding that.
+Transport plugins
+~~~~~~~~~~~~~~~~~
+Transport plugins allow you to override how Raven sends the data into Sentry.
+A transport is added based on the dsn passed into config, e.g `Raven.config('http://abc@example.com:80/2')` will use the transport registered for `http`.
+
+The default transport plugin uses inserts an image thus generating a `GET` request to Sentry.
+There is also a transport plugin available for `CORS XHR` in situations where the `Origin` and `Referer` headers won't get set properly for `GET` requests (e.g when the browser is executing from a `file://` URI).
+
+Registering new transport plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+New methods of transports can be added to Raven using
+
+.. code-block:: javascript
+
+ Raven.registerTransport('https+post', {
+ /*
+ * Configure the transport protocol using the provided config
+ */
+ setup: function(dsn, triggerEvent){
+ // snip
+ },
+
+ /*
+ * Send the data to Sentry.
+ */
+ send: function(data, endpoint){
+ // snip
+ }
+ });
+
+
+A transport's `setup` method receives a `triggerEvent` parameter which
+can be used to trigger an event when the request had succeeded or failed,
+using:
+
+.. code-block:: javascript
+
+ img.onload = function success() {
+ triggerEvent('success', {
+ data: data,
+ src: src
+ });
+ };
+ img.onerror = img.onabort = function failure() {
+ triggerEvent('failure', {
+ data: data,
+ src: src
+ });
+ };
+
+
All Plugins
~~~~~~~~~~~
diff --git a/plugins/v8-xhr.js b/plugins/v8-xhr.js
new file mode 100644
index 000000000000..8472437d7262
--- /dev/null
+++ b/plugins/v8-xhr.js
@@ -0,0 +1,74 @@
+/**
+ * Chrome/V8 XHR Post plugin
+ *
+ * Allows raven-js to send log entries using XHR and CORS for
+ * environments where the Origin and Referer headers are missing (e.g Chrome plugins).
+ *
+ * Usage: Raven.config('https+post://...');
+ * */
+
+;(function(window, Raven, undefined){
+ 'use strict';
+ var V8Transport = window.V8Transport = {
+ setup: function(dsn, triggerEvent){
+ if(!this.hasCORS() && window.console && console.error){
+ console.error('This browser lacks support for CORS. Falling back to the default transport');
+ delete dsn.pass;
+ HTTPGetTransport.setup(dsn);
+ return HTTPGetTransport;
+ }
+
+ if(!dsn.pass)
+ throw new RavenConfigError('The https+post V8 transport needs the private key to be set in the DSN.');
+
+ this.triggerEvent = triggerEvent;
+ this.dsn = dsn;
+ return this;
+ },
+
+ hasCORS: function(){
+ return 'withCredentials' in new XMLHttpRequest();
+ },
+
+ getAuthString: function(){
+ if (this.cachedAuth) return this.cachedAuth;
+
+ var qs = [
+ 'sentry_version=4',
+ 'sentry_client=raven-js/' + Raven.VERSION,
+ 'sentry_key=' + this.dsn.user,
+ 'sentry_secret=' + this.dsn.pass
+ ];
+
+ return this.cachedAuth = 'Sentry ' + qs.join(',');
+ },
+
+ send: function(data, endpoint){
+ var xhr = new XMLHttpRequest(),
+ triggerEvent = this.triggerEvent;
+
+
+ xhr.open('POST', endpoint, true);
+ xhr.setRequestHeader('X-Sentry-Auth', this.getAuthString());
+ xhr.setRequestHeader('Content-Type', 'application/json; charset=utf-8');
+
+ xhr.onload = function success() {
+ triggerEvent('success', {
+ data: data,
+ src: endpoint
+ });
+ };
+ xhr.onerror = xhr.onabort = function failure() {
+ triggerEvent('failure', {
+ data: data,
+ src: endpoint
+ });
+ };
+
+ xhr.send(JSON.stringify(data));
+ }
+ }
+
+ Raven.registerTransport('https+post', V8Transport);
+}(this, Raven));
+
diff --git a/src/raven.js b/src/raven.js
index abb43f3e8203..d0ed2a593bb3 100644
--- a/src/raven.js
+++ b/src/raven.js
@@ -1,5 +1,49 @@
'use strict';
+/*
+ * Default transport.
+ * */
+var HTTPGetTransport = {
+ setup: function(dsn, triggerEvent){
+ if (dsn.pass)
+ throw new RavenConfigError('Do not specify your private key in the DSN!');
+ this.dsn = dsn;
+ this.triggerEvent = triggerEvent;
+ },
+ send: function(data, endpoint){
+ var img = new Image(),
+ triggerEvent = this.triggerEvent,
+ src = endpoint + this.getAuthQueryString() + '&sentry_data=' + encodeURIComponent(JSON.stringify(data));
+
+ img.onload = function success() {
+ triggerEvent('success', {
+ data: data,
+ src: src
+ });
+ };
+ img.onerror = img.onabort = function failure() {
+ triggerEvent('failure', {
+ data: data,
+ src: src
+ });
+ };
+ img.src = src;
+ },
+
+ getAuthQueryString: function() {
+ if (this.cachedAuth) return this.cachedAuth;
+ var qs = [
+ 'sentry_version=4',
+ 'sentry_client=raven-js/' + Raven.VERSION
+ ];
+ if (globalKey) {
+ qs.push('sentry_key=' + this.dsn.user);
+ }
+
+ return this.cachedAuth = '?' + qs.join('&');
+ }
+};
+
// First, check for JSON support
// If there is no JSON, we no-op the core features of Raven
// since JSON is required to encode the payload
@@ -10,6 +54,8 @@ var _Raven = window.Raven,
globalUser,
globalKey,
globalProject,
+ globalTransports = { 'default': HTTPGetTransport },
+ globalTransport = null,
globalOptions = {
logger: 'javascript',
ignoreErrors: [],
@@ -21,6 +67,7 @@ var _Raven = window.Raven,
extra: {}
};
+
/*
* The core Raven singleton
*
@@ -95,7 +142,9 @@ var Raven = {
'/' + path + 'api/' + globalProject + '/store/';
if (uri.protocol) {
- globalServer = uri.protocol + ':' + globalServer;
+ // Only use the first part of the url if it contains
+ // a plugin identifier, e.g https+post -> https
+ globalServer = uri.protocol.split('+')[0] + ':' + globalServer;
}
if (globalOptions.fetchContext) {
@@ -108,6 +157,13 @@ var Raven = {
TraceKit.collectWindowErrors = !!globalOptions.collectWindowErrors;
+ globalTransport = uri.protocol && globalTransports[uri.protocol] ? globalTransports[uri.protocol] : globalTransports['default'];
+
+ // Allow transports to fall back to other transport protocols,
+ // if the chosen one isn't supported
+ var transport = globalTransport.setup(uri, triggerEvent);
+ globalTransport = transport ? transport : globalTransport;
+
// return for chaining
return Raven;
},
@@ -128,6 +184,23 @@ var Raven = {
return Raven;
},
+
+
+ /*
+ * Register a transport plugin for pushing data into Sentry.
+ * Transport plugins are chosen based upon the DSN passed into Raven.configure,
+ * e.g Raven.configure('http://...') will use the plugin registered for 'http'.
+ *
+ * @param {string} protocol A protocol identifier in the DSN, e.g http or https+post
+ * @param {object} transport An implementation of the transport. Must implement the `setup(dsn)` and `send(data, endpoint)` methods.
+ */
+ registerTransport: function(protocol, transport){
+ if(globalTransports[protocol]) throw new RavenConfigError('Protocol ' + protocol + ' already has a registered transport method');
+
+ globalTransports[protocol] = transport;
+ return Raven;
+ },
+
/*
* Wrap code within a context so Raven can capture errors
* reliably across domains that is executed immediately.
@@ -321,7 +394,7 @@ function triggerEvent(eventType, options) {
}
var dsnKeys = 'source protocol user pass host port path'.split(' '),
- dsnPattern = /^(?:(\w+):)?\/\/(\w+)(:\w+)?@([\w\.-]+)(?::(\d+))?(\/.*)/;
+ dsnPattern = /^(?:(\w+|\w+\+\w+)?:)?\/\/(\w+)(?::(\w+))?@([\w\.-]+)(?::(\d+))?(\/.*)/;
function RavenConfigError(message) {
this.name = 'RavenConfigError';
@@ -334,7 +407,7 @@ RavenConfigError.prototype.constructor = RavenConfigError;
function parseDSN(str) {
var m = dsnPattern.exec(str),
dsn = {},
- i = 7;
+ i = dsnKeys.length;
try {
while (i--) dsn[dsnKeys[i]] = m[i] || '';
@@ -342,9 +415,6 @@ function parseDSN(str) {
throw new RavenConfigError('Invalid DSN: ' + str);
}
- if (dsn.pass)
- throw new RavenConfigError('Do not specify your private key in the DSN!');
-
return dsn;
}
@@ -395,23 +465,6 @@ function each(obj, callback) {
}
}
-var cachedAuth;
-
-function getAuthQueryString() {
- if (cachedAuth) return cachedAuth;
-
- var qs = [
- 'sentry_version=4',
- 'sentry_client=raven-js/' + Raven.VERSION
- ];
- if (globalKey) {
- qs.push('sentry_key=' + globalKey);
- }
-
- cachedAuth = '?' + qs.join('&');
- return cachedAuth;
-}
-
function handleStackInfo(stackInfo, options) {
var frames = [];
@@ -619,22 +672,7 @@ function send(data) {
function makeRequest(data) {
- var img = new Image(),
- src = globalServer + getAuthQueryString() + '&sentry_data=' + encodeURIComponent(JSON.stringify(data));
-
- img.onload = function success() {
- triggerEvent('success', {
- data: data,
- src: src
- });
- };
- img.onerror = img.onabort = function failure() {
- triggerEvent('failure', {
- data: data,
- src: src
- });
- };
- img.src = src;
+ globalTransport.send(data, globalServer);
}
function isSetup() {
diff --git a/test/index.html b/test/index.html
index b5188ed06000..7d1e189ee59f 100644
--- a/test/index.html
+++ b/test/index.html
@@ -31,6 +31,7 @@
+
diff --git a/test/raven.test.js b/test/raven.test.js
index 5d99e37dfca5..02aae164f49d 100644
--- a/test/raven.test.js
+++ b/test/raven.test.js
@@ -5,6 +5,8 @@ function flushRavenState() {
globalServer = undefined;
globalUser = undefined;
globalProject = undefined;
+ globalTransports = { default: HTTPGetTransport };
+ globalTransport = undefined;
globalOptions = {
logger: 'javascript',
ignoreErrors: [],
@@ -18,6 +20,7 @@ function flushRavenState() {
Raven.uninstall();
}
+var noop = function(){};
var imageCache = [];
window.Image = function Image() {
imageCache.push(this);
@@ -29,6 +32,8 @@ if (typeof window.console === 'undefined') {
}
var SENTRY_DSN = 'http://abc@example.com:80/2';
+var PLUGIN_DSN = 'foo+bar://abc@example.com:80/2';
+var V8_DSN = 'https+post://abc:pwd@example.com:80/2';
function setupRaven() {
Raven.config(SENTRY_DSN);
@@ -122,6 +127,7 @@ describe('globals', function() {
this.sinon.stub(console, 'error');
assert.isFalse(isSetup());
assert.isTrue(console.error.calledOnce);
+ console.error.restore();
});
it('should return true when everything is all gravy', function() {
@@ -130,19 +136,6 @@ describe('globals', function() {
});
});
- describe('getAuthQueryString', function() {
- it('should return a properly formatted string and cache it', function() {
- var expected = '?sentry_version=4&sentry_client=raven-js/<%= pkg.version %>&sentry_key=abc';
- assert.strictEqual(getAuthQueryString(), expected);
- assert.strictEqual(cachedAuth, expected);
- });
-
- it('should return cached value when it exists', function() {
- cachedAuth = 'lol';
- assert.strictEqual(getAuthQueryString(), 'lol');
- });
- });
-
describe('parseDSN', function() {
it('should do what it advertises', function() {
var pieces = parseDSN('http://abc@example.com:80/2');
@@ -171,16 +164,6 @@ describe('globals', function() {
assert.strictEqual(pieces.host, 'matt-robenolt.com');
});
- it('should raise a RavenConfigError when setting a password', function() {
- try {
- parseDSN('http://user:pass@example.com/2');
- } catch(e) {
- return assert.equal(e.name, 'RavenConfigError');
- }
- // shouldn't hit this
- assert.isTrue(false);
- });
-
it('should raise a RavenConfigError with an invalid DSN', function() {
try {
parseDSN('lol');
@@ -190,6 +173,7 @@ describe('globals', function() {
// shouldn't hit this
assert.isTrue(false);
});
+
});
describe('normalizeFrame', function() {
@@ -713,7 +697,7 @@ describe('globals', function() {
describe('makeRequest', function() {
it('should load an Image', function() {
imageCache = [];
- this.sinon.stub(window, 'getAuthQueryString').returns('?lol');
+ this.sinon.stub(globalTransport, 'getAuthQueryString').returns('?lol');
globalServer = 'http://localhost/';
makeRequest({foo: 'bar'});
@@ -1247,4 +1231,185 @@ describe('Raven (public API)', function() {
assert.isFalse(TraceKit.report.called);
});
});
+
+ describe('The transport plugin API', function(){
+ beforeEach(function() {
+ setupRaven();
+ globalOptions.fetchContext = true;
+ });
+
+ afterEach(function() {
+ flushRavenState();
+ });
+
+ it('should be able to register new transport plugins', function(){
+ // Flush raven so we can run .config on it again.
+ flushRavenState();
+
+ var setupSpy = this.sinon.spy();
+ Raven.registerTransport('foo+bar', {
+ setup: setupSpy,
+ send: function(){}
+ });
+
+ Raven.config('foo+bar://abc@example.com:80/2');
+
+ assert.isTrue(setupSpy.calledOnce);
+ });
+
+ it('should use the correct plugin, without plugin name when making a request', function(){
+ // Flush raven so we can run .config on it again.
+ flushRavenState();
+
+ var sendSpy = this.sinon.spy();
+ Raven.registerTransport('foo+bar', {
+ setup: function(dsn){
+ return this;
+ },
+ send: sendSpy
+ });
+
+ Raven.config('foo+bar://abc@example.com:80/2');
+ makeRequest({ foo: 'bar' })
+
+ assert.deepEqual(sendSpy.args[0][0], { foo: 'bar' });
+ assert.strictEqual(sendSpy.args[0][1], 'foo://example.com:80/api/2/store/');
+ });
+
+ it('should run the plugins\' setup method and provide the dsn and triggerEvent function', function(){
+ // Flush raven so we can run .config on it again.
+ flushRavenState();
+
+ var setupSpy = this.sinon.spy();
+ Raven.registerTransport('foo+bar', {
+ setup: setupSpy,
+ send: function(){}
+ });
+
+ Raven.config(PLUGIN_DSN);
+ var dsn = parseDSN('foo+bar://abc@example.com:80/2');
+
+ assert.deepEqual(setupSpy.args[0][0], dsn);
+ assert.strictEqual(setupSpy.args[0][1], triggerEvent);
+ }),
+
+ it('should throw an error if a transport plugin is already registerd for a protocol', function(){
+ Raven.registerTransport('foo+bar', {
+ setup: noop,
+ send: noop
+ });
+ try {
+ Raven.registerTransport('foo+bar', {
+ setup: noop,
+ send: noop
+ });
+ } catch(e){
+ return assert.equal(e.name, 'RavenConfigError');
+ return assert.equal(e.name, 'Protocol foo+bar already has a registered transport method');
+ }
+
+ });
+
+ it('should allow a plugin to use a fallback (e.g is support is missing)', function(){
+ // Flush raven so we can run .config on it again.
+ flushRavenState();
+
+ var pluginA = {
+ setup: noop,
+ send: noop
+ };
+ var pluginB = {
+ setup: function(){
+ return pluginA;
+ },
+ send: noop
+ };
+
+ Raven.registerTransport('foo+bar', pluginB);
+ Raven.config(PLUGIN_DSN);
+
+ assert.deepEqual(globalTransport, pluginA);
+ });
+ describe('the default transport plugin', function(){
+
+ it('should set a default transport plugin', function(){
+ assert.deepEqual(globalTransport, HTTPGetTransport);
+ });
+
+ it('should raise a RavenConfigError when setting a password', function() {
+ try {
+ HTTPGetTransport.setup(parseDSN('http://user:pass@example.com/2'));
+ } catch(e) {
+ return assert.equal(e.name, 'RavenConfigError');
+ }
+ // shouldn't hit this
+ assert.isTrue(false);
+ });
+
+ describe('getAuthQueryString', function() {
+ it('should return a properly formatted string and cache it', function() {
+ var expected = '?sentry_version=4&sentry_client=raven-js/<%= pkg.version %>&sentry_key=abc';
+ assert.strictEqual(globalTransport.getAuthQueryString(), expected);
+ assert.strictEqual(globalTransport.cachedAuth, expected);
+ });
+
+ it('should return cached value when it exists', function() {
+ globalTransport.cachedAuth = 'lol';
+ assert.strictEqual(globalTransport.getAuthQueryString(), 'lol');
+ });
+ });
+
+ });
+
+ describe('the v8 transport plugin', function(){
+ it('should throw an RavenConfigError if the password is missing', function(){
+ flushRavenState();
+ Raven.registerTransport('https+post', V8Transport);
+ try {
+ Raven.config('https+post://abc@example.com:80/2');
+ } catch(e){
+ return assert.strictEqual(e.name, 'RavenConfigError');
+ return assert.strictEqual(e.name, 'The https+post V8 transport needs the private key to be set in the DSN.');
+ }
+ // We should never hit this
+ assert.isTrue(false);
+ });
+
+ it('should fall back to the GET plugin if CORS isn\'t supported', function(){
+ flushRavenState();
+ Raven.registerTransport('https+post', V8Transport);
+
+ var stub = this.sinon.stub(V8Transport, 'hasCORS');
+ this.sinon.stub(console, 'error');
+ stub.returns(false);
+
+ Raven.config(V8_DSN);
+
+ assert.deepEqual(globalTransport, HTTPGetTransport);
+ assert.isTrue(console.error.calledOnce);
+
+ console.error.restore();
+ });
+
+ it('should send requests using xhr post', function(){
+ flushRavenState();
+ Raven.registerTransport('https+post', V8Transport);
+ Raven.config(V8_DSN);
+
+ var xhr = this.sinon.useFakeXMLHttpRequest();
+
+ makeRequest({ foo: 'bar' });
+
+ assert.strictEqual(xhr.requests[0].url, 'https://example.com:80/api/2/store/');
+ assert.deepEqual(xhr.requests[0].requestHeaders, {
+ 'X-Sentry-Auth': "Sentry sentry_version=4,sentry_client=raven-js/<%= pkg.version %>,sentry_key=abc,sentry_secret=pwd",
+ 'Content-Type': "application/json;charset=utf-8"
+ });
+ assert.strictEqual(xhr.requests[0].requestBody, JSON.stringify({ foo: 'bar' }));
+
+ xhr.restore();
+ });
+ });
+ });
+
});