Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for transport plugins #183

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions docs/plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~
Expand Down
74 changes: 74 additions & 0 deletions plugins/v8-xhr.js
Original file line number Diff line number Diff line change
@@ -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 = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not call this one XHRTransport?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because sadly, this doesn't just work everywhere that XHR is accepted, so this is specific to V8.

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');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the window.console && console.error check be only around this console.error() call?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

delete dsn.pass;
HTTPGetTransport.setup(dsn);
return HTTPGetTransport;
}

if(!dsn.pass)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend also checking if the user wants https here so they could also use http+post

if(!dsn.pass && dsn.protocol == 'https+post')

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
];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make pass optional for people who just want http.

var qs = [
    'sentry_version=4',
    'sentry_client=raven-js/' + Raven.VERSION,
    'sentry_key=' + this.dsn.user
];
if (this.dsn.pass) {
    qs.push('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);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I would add

Raven.registerTransport('http+post', V8Transport);

}(this, Raven));

116 changes: 77 additions & 39 deletions src/raven.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
'use strict';

/*
* Default transport.
* */
var HTTPGetTransport = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.. and this one ImageTransport?

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
Expand All @@ -10,6 +54,8 @@ var _Raven = window.Raven,
globalUser,
globalKey,
globalProject,
globalTransports = { 'default': HTTPGetTransport },
globalTransport = null,
globalOptions = {
logger: 'javascript',
ignoreErrors: [],
Expand All @@ -21,6 +67,7 @@ var _Raven = window.Raven,
extra: {}
};


/*
* The core Raven singleton
*
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
},
Expand All @@ -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.
Expand Down Expand Up @@ -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';
Expand All @@ -334,17 +407,14 @@ 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] || '';
} catch(e) {
throw new RavenConfigError('Invalid DSN: ' + str);
}

if (dsn.pass)
throw new RavenConfigError('Do not specify your private key in the DSN!');

return dsn;
}

Expand Down Expand Up @@ -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 = [];

Expand Down Expand Up @@ -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() {
Expand Down
1 change: 1 addition & 0 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<!-- Raven -->
<script src="../vendor/TraceKit/tracekit.js"></script>
<script src="../src/raven.js"></script>
<script src="../plugins/v8-xhr.js"></script>

<!-- Tests -->
<script src="raven.test.js"></script>
Expand Down
Loading