diff --git a/src/raven.js b/src/raven.js
index 44fb3421cc4c..ffb8b3c3071b 100644
--- a/src/raven.js
+++ b/src/raven.js
@@ -18,6 +18,7 @@ var truncate = utils.truncate;
var urlencode = utils.urlencode;
var uuid4 = utils.uuid4;
var htmlElementAsString = utils.htmlElementAsString;
+var parseUrl = utils.parseUrl;
var dsnKeys = 'source protocol user pass host port path'.split(' '),
dsnPattern = /^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/;
@@ -63,7 +64,8 @@ function Raven() {
this._breadcrumbs = [];
this._breadcrumbLimit = 20;
this._lastCapturedEvent = null;
- this._lastHref = window.location && location.href;
+ this._location = window.location;
+ this._lastHref = this._location && this._location.href;
for (var method in this._originalConsole) { // eslint-disable-line guard-for-in
this._originalConsoleMethods[method] = this._originalConsole[method];
@@ -642,6 +644,35 @@ Raven.prototype = {
};
},
+ /**
+ * Captures a breadcrumb of type "navigation", normalizing input URLs
+ * @param to the originating URL
+ * @param from the target URL
+ * @private
+ */
+ _captureUrlChange: function(from, to) {
+ var parsedLoc = parseUrl(this._location.href);
+ var parsedTo = parseUrl(to);
+ var parsedFrom = parseUrl(from);
+
+ // because onpopstate only tells you the "new" (to) value of location.href, and
+ // not the previous (from) value, we need to track the value of the current URL
+ // state ourselves
+ this._lastHref = to;
+
+ // Use only the path component of the URL if the URL matches the current
+ // document (almost all the time when using pushState)
+ if (parsedLoc.protocol === parsedTo.protocol && parsedLoc.host === parsedTo.host)
+ to = parsedTo.path;
+ if (parsedLoc.protocol === parsedFrom.protocol && parsedLoc.host === parsedFrom.host)
+ from = parsedFrom.path;
+
+ this.captureBreadcrumb('navigation', {
+ to: to,
+ from: from
+ });
+ },
+
/**
* Install any queued plugins
*/
@@ -795,15 +826,9 @@ Raven.prototype = {
// TODO: remove onpopstate handler on uninstall()
var oldOnPopState = window.onpopstate;
window.onpopstate = function () {
- self.captureBreadcrumb('navigation', {
- from: self._lastHref,
- to: location.href
- });
+ var currentHref = self._location.href;
+ self._captureUrlChange(self._lastHref, currentHref);
- // because onpopstate only tells you the "new" (to) value of location.href, and
- // not the previous (from) value, we need to track the value of location.href
- // ourselves
- self._lastHref = location.href;
if (oldOnPopState) {
return oldOnPopState.apply(this, arguments);
}
@@ -814,13 +839,14 @@ Raven.prototype = {
// params to preserve 0 arity
return function(/* state, title, url */) {
var url = arguments.length > 2 ? arguments[2] : undefined;
- self.captureBreadcrumb('navigation', {
- to: url,
- from: location.href
- });
- if (url) self._lastHref = url;
+
+ // url argument is optional
+ if (url) {
+ self._captureUrlChange(self._lastHref, url);
+ }
+
return origPushState.apply(this, arguments);
- }
+ };
});
}
diff --git a/src/utils.js b/src/utils.js
index ae0c05800924..73bae2eab972 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -107,6 +107,18 @@ function urlencode(o) {
return pairs.join('&');
}
+// borrowed from https://tools.ietf.org/html/rfc3986#appendix-B
+// intentionally using regex and not href parsing trick because React Native and other
+// environments where DOM might not be available
+function parseUrl(url) {
+ var match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/);
+ if (!match) return {};
+ return {
+ protocol: match[2],
+ host: match[4],
+ path: match[5]
+ };
+}
function uuid4() {
var crypto = window.crypto || window.msCrypto;
@@ -173,5 +185,6 @@ module.exports = {
joinRegExp: joinRegExp,
urlencode: urlencode,
uuid4: uuid4,
- htmlElementAsString: htmlElementAsString
+ htmlElementAsString: htmlElementAsString,
+ parseUrl: parseUrl
};
diff --git a/test/integration/test.js b/test/integration/test.js
index 2d4814645131..396288bf5a20 100644
--- a/test/integration/test.js
+++ b/test/integration/test.js
@@ -261,7 +261,7 @@ describe('integration', function () {
// since this is what jQuery does
// https://github.com/jquery/jquery/blob/master/src/ajax/xhr.js#L37
- xhr.open('GET', 'example.json')
+ xhr.open('GET', 'example.json');
xhr.onreadystatechange = function () {
setTimeout(done);
// replace onreadystatechange with no-op so exception doesn't
diff --git a/test/raven.test.js b/test/raven.test.js
index d091440123ce..87d7471c580b 100644
--- a/test/raven.test.js
+++ b/test/raven.test.js
@@ -2088,6 +2088,26 @@ describe('Raven (public API)', function() {
});
});
+ describe('._captureUrlChange', function () {
+ it('should create a new breadcrumb from its "from" and "to" arguments', function () {
+ Raven._breadcrumbs = [];
+ Raven._captureUrlChange('/foo', '/bar');
+ assert.deepEqual(Raven._breadcrumbs, [
+ { type: 'navigation', timestamp: 0.1, data: { from: '/foo', to: '/bar' }}
+ ]);
+ });
+
+ it('should strip protocol/host if passed URLs share the same origin as location.href', function () {
+ Raven._location = { href: 'http://example.com/foo' };
+ Raven._breadcrumbs = [];
+
+ Raven._captureUrlChange('http://example.com/foo', 'http://example.com/bar');
+ assert.deepEqual(Raven._breadcrumbs, [
+ { type: 'navigation', timestamp: 0.1, data: { from: '/foo', to: '/bar' }}
+ ]);
+ });
+ });
+
describe('.Raven.isSetup', function() {
it('should work as advertised', function() {
var isSetup = this.sinon.stub(Raven, 'isSetup');
diff --git a/test/utils.test.js b/test/utils.test.js
index 79fc8cc22ead..d28e37038e86 100644
--- a/test/utils.test.js
+++ b/test/utils.test.js
@@ -17,6 +17,7 @@ var objectMerge = utils.objectMerge;
var truncate = utils.truncate;
var urlencode = utils.urlencode;
var htmlElementAsString = utils.htmlElementAsString;
+var parseUrl = utils.parseUrl;
describe('utils', function () {
describe('isUndefined', function() {
@@ -145,7 +146,35 @@ describe('utils', function () {
}
}), '
');
});
+ });
+
+ describe('parseUrl', function () {
+ it('should parse fully qualified URLs', function () {
+ assert.deepEqual(parseUrl('http://example.com/foo'), {
+ host: 'example.com',
+ path: '/foo',
+ protocol: 'http'
+ });
+ assert.deepEqual(parseUrl('//example.com/foo'), {
+ host: 'example.com',
+ path: '/foo',
+ protocol: undefined
+ });
+ });
- it
+ it('should parse partial URLs, e.g. path only', function () {
+ assert.deepEqual(parseUrl('/foo'), {
+ host: undefined,
+ protocol: undefined,
+ path: '/foo'
+ });
+ assert.deepEqual(parseUrl('example.com/foo'), {
+ host: undefined,
+ protocol: undefined,
+ path: 'example.com/foo'
+ // this is correct! pushState({}, '', 'example.com/foo') would take you
+ // from example.com => example.com/example.com/foo (valid url).
+ });
+ });
});
});