Skip to content

Commit ee024e6

Browse files
committed
Merge pull request #556 from getsentry/fix-url-breadcrumbs
Strip protocol + host from breadcrumb URLs if they match current URL
2 parents b61bc51 + 1692503 commit ee024e6

File tree

5 files changed

+106
-18
lines changed

5 files changed

+106
-18
lines changed

src/raven.js

Lines changed: 41 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ var truncate = utils.truncate;
1818
var urlencode = utils.urlencode;
1919
var uuid4 = utils.uuid4;
2020
var htmlElementAsString = utils.htmlElementAsString;
21+
var parseUrl = utils.parseUrl;
2122

2223
var dsnKeys = 'source protocol user pass host port path'.split(' '),
2324
dsnPattern = /^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/;
@@ -63,7 +64,8 @@ function Raven() {
6364
this._breadcrumbs = [];
6465
this._breadcrumbLimit = 20;
6566
this._lastCapturedEvent = null;
66-
this._lastHref = window.location && location.href;
67+
this._location = window.location;
68+
this._lastHref = this._location && this._location.href;
6769

6870
for (var method in this._originalConsole) { // eslint-disable-line guard-for-in
6971
this._originalConsoleMethods[method] = this._originalConsole[method];
@@ -642,6 +644,35 @@ Raven.prototype = {
642644
};
643645
},
644646

647+
/**
648+
* Captures a breadcrumb of type "navigation", normalizing input URLs
649+
* @param to the originating URL
650+
* @param from the target URL
651+
* @private
652+
*/
653+
_captureUrlChange: function(from, to) {
654+
var parsedLoc = parseUrl(this._location.href);
655+
var parsedTo = parseUrl(to);
656+
var parsedFrom = parseUrl(from);
657+
658+
// because onpopstate only tells you the "new" (to) value of location.href, and
659+
// not the previous (from) value, we need to track the value of the current URL
660+
// state ourselves
661+
this._lastHref = to;
662+
663+
// Use only the path component of the URL if the URL matches the current
664+
// document (almost all the time when using pushState)
665+
if (parsedLoc.protocol === parsedTo.protocol && parsedLoc.host === parsedTo.host)
666+
to = parsedTo.path;
667+
if (parsedLoc.protocol === parsedFrom.protocol && parsedLoc.host === parsedFrom.host)
668+
from = parsedFrom.path;
669+
670+
this.captureBreadcrumb('navigation', {
671+
to: to,
672+
from: from
673+
});
674+
},
675+
645676
/**
646677
* Install any queued plugins
647678
*/
@@ -795,15 +826,9 @@ Raven.prototype = {
795826
// TODO: remove onpopstate handler on uninstall()
796827
var oldOnPopState = window.onpopstate;
797828
window.onpopstate = function () {
798-
self.captureBreadcrumb('navigation', {
799-
from: self._lastHref,
800-
to: location.href
801-
});
829+
var currentHref = self._location.href;
830+
self._captureUrlChange(self._lastHref, currentHref);
802831

803-
// because onpopstate only tells you the "new" (to) value of location.href, and
804-
// not the previous (from) value, we need to track the value of location.href
805-
// ourselves
806-
self._lastHref = location.href;
807832
if (oldOnPopState) {
808833
return oldOnPopState.apply(this, arguments);
809834
}
@@ -814,13 +839,14 @@ Raven.prototype = {
814839
// params to preserve 0 arity
815840
return function(/* state, title, url */) {
816841
var url = arguments.length > 2 ? arguments[2] : undefined;
817-
self.captureBreadcrumb('navigation', {
818-
to: url,
819-
from: location.href
820-
});
821-
if (url) self._lastHref = url;
842+
843+
// url argument is optional
844+
if (url) {
845+
self._captureUrlChange(self._lastHref, url);
846+
}
847+
822848
return origPushState.apply(this, arguments);
823-
}
849+
};
824850
});
825851
}
826852

src/utils.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@ function urlencode(o) {
107107
return pairs.join('&');
108108
}
109109

110+
// borrowed from https://tools.ietf.org/html/rfc3986#appendix-B
111+
// intentionally using regex and not <a/> href parsing trick because React Native and other
112+
// environments where DOM might not be available
113+
function parseUrl(url) {
114+
var match = url.match(/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/);
115+
if (!match) return {};
116+
return {
117+
protocol: match[2],
118+
host: match[4],
119+
path: match[5]
120+
};
121+
}
110122
function uuid4() {
111123
var crypto = window.crypto || window.msCrypto;
112124

@@ -173,5 +185,6 @@ module.exports = {
173185
joinRegExp: joinRegExp,
174186
urlencode: urlencode,
175187
uuid4: uuid4,
176-
htmlElementAsString: htmlElementAsString
188+
htmlElementAsString: htmlElementAsString,
189+
parseUrl: parseUrl
177190
};

test/integration/test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ describe('integration', function () {
261261
// since this is what jQuery does
262262
// https://github.com/jquery/jquery/blob/master/src/ajax/xhr.js#L37
263263

264-
xhr.open('GET', 'example.json')
264+
xhr.open('GET', 'example.json');
265265
xhr.onreadystatechange = function () {
266266
setTimeout(done);
267267
// replace onreadystatechange with no-op so exception doesn't

test/raven.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2088,6 +2088,26 @@ describe('Raven (public API)', function() {
20882088
});
20892089
});
20902090

2091+
describe('._captureUrlChange', function () {
2092+
it('should create a new breadcrumb from its "from" and "to" arguments', function () {
2093+
Raven._breadcrumbs = [];
2094+
Raven._captureUrlChange('/foo', '/bar');
2095+
assert.deepEqual(Raven._breadcrumbs, [
2096+
{ type: 'navigation', timestamp: 0.1, data: { from: '/foo', to: '/bar' }}
2097+
]);
2098+
});
2099+
2100+
it('should strip protocol/host if passed URLs share the same origin as location.href', function () {
2101+
Raven._location = { href: 'http://example.com/foo' };
2102+
Raven._breadcrumbs = [];
2103+
2104+
Raven._captureUrlChange('http://example.com/foo', 'http://example.com/bar');
2105+
assert.deepEqual(Raven._breadcrumbs, [
2106+
{ type: 'navigation', timestamp: 0.1, data: { from: '/foo', to: '/bar' }}
2107+
]);
2108+
});
2109+
});
2110+
20912111
describe('.Raven.isSetup', function() {
20922112
it('should work as advertised', function() {
20932113
var isSetup = this.sinon.stub(Raven, 'isSetup');

test/utils.test.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ var objectMerge = utils.objectMerge;
1717
var truncate = utils.truncate;
1818
var urlencode = utils.urlencode;
1919
var htmlElementAsString = utils.htmlElementAsString;
20+
var parseUrl = utils.parseUrl;
2021

2122
describe('utils', function () {
2223
describe('isUndefined', function() {
@@ -145,7 +146,35 @@ describe('utils', function () {
145146
}
146147
}), '<img id="image-3" title="A picture of an apple" />');
147148
});
149+
});
150+
151+
describe('parseUrl', function () {
152+
it('should parse fully qualified URLs', function () {
153+
assert.deepEqual(parseUrl('http://example.com/foo'), {
154+
host: 'example.com',
155+
path: '/foo',
156+
protocol: 'http'
157+
});
158+
assert.deepEqual(parseUrl('//example.com/foo'), {
159+
host: 'example.com',
160+
path: '/foo',
161+
protocol: undefined
162+
});
163+
});
148164

149-
it
165+
it('should parse partial URLs, e.g. path only', function () {
166+
assert.deepEqual(parseUrl('/foo'), {
167+
host: undefined,
168+
protocol: undefined,
169+
path: '/foo'
170+
});
171+
assert.deepEqual(parseUrl('example.com/foo'), {
172+
host: undefined,
173+
protocol: undefined,
174+
path: 'example.com/foo'
175+
// this is correct! pushState({}, '', 'example.com/foo') would take you
176+
// from example.com => example.com/example.com/foo (valid url).
177+
});
178+
});
150179
});
151180
});

0 commit comments

Comments
 (0)