Skip to content

Commit 353c011

Browse files
committed
Tests, fixes for mouse click breadcrumb capturing
1 parent 29bc465 commit 353c011

File tree

3 files changed

+199
-17
lines changed

3 files changed

+199
-17
lines changed

src/raven.js

Lines changed: 51 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ function Raven() {
6262
this._wrappedBuiltIns = [];
6363
this._breadcrumbs = [];
6464
this._breadcrumbLimit = 20;
65+
this._lastCapturedEvent = null;
6566
this._lastHref = window.location && location.href;
6667

6768
for (var method in this._originalConsole) { // eslint-disable-line guard-for-in
@@ -207,7 +208,6 @@ Raven.prototype = {
207208
*/
208209
wrap: function(options, func) {
209210
var self = this;
210-
211211
// 1 argument has been passed, and it's not a function
212212
// so just return it
213213
if (isUndefined(func) && !isFunction(options)) {
@@ -260,21 +260,29 @@ Raven.prototype = {
260260
}
261261
}
262262

263+
this._imitate(wrapped, func);
264+
265+
func.__raven_wrapper__ = wrapped;
266+
// Signal that this function has been wrapped already
267+
// for both debugging and to prevent it to being wrapped twice
268+
wrapped.__raven__ = true;
269+
wrapped.__inner__ = func;
270+
271+
return wrapped;
272+
},
273+
274+
/**
275+
* Give a wrapper function the properties/prototype
276+
* of the wrapepd (inner) function
277+
*/
278+
_imitate: function (wrapped, func) {
263279
// copy over properties of the old function
264280
for (var property in func) {
265281
if (hasKey(func, property)) {
266282
wrapped[property] = func[property];
267283
}
268284
}
269-
func.__raven_wrapper__ = wrapped;
270-
271285
wrapped.prototype = func.prototype;
272-
273-
// Signal that this function has been wrapped already
274-
// for both debugging and to prevent it to being wrapped twice
275-
wrapped.__raven__ = true;
276-
wrapped.__inner__ = func;
277-
278286
return wrapped;
279287
},
280288

@@ -611,24 +619,39 @@ Raven.prototype = {
611619

612620
/**
613621
* Wraps addEventListener to capture breadcrumbs
614-
* @param elem the element addEventListener was called on
615-
* @param evt the event name (e.g. "click")
622+
* @param evtName the event name (e.g. "click")
616623
* @param fn the function being wrapped
617624
* @returns {Function}
618625
* @private
619626
*/
620-
_wrapEventHandlerForBreadcrumbs: function(elem, evt, fn) {
627+
_wrapEventHandlerForBreadcrumbs: function(evtName, fn) {
621628
var self = this;
622-
return function () {
629+
function wrapped(evt) {
630+
// It's possible this handler might trigger multiple times for the same
631+
// event (e.g. event propagation through node ancestors). Ignore if we've
632+
// already captured the event.
633+
if (self._lastCapturedEvent === evt)
634+
return;
635+
636+
self._lastCapturedEvent = evt;
637+
var elem = evt.target;
623638
self.captureBreadcrumb({
624639
type: 'ui_event',
625640
data: {
626-
type: evt,
641+
type: evtName,
627642
target: htmlElementAsString(elem)
628643
}
629644
});
630-
return fn.apply(this, arguments);
645+
if (fn) return fn.apply(this, arguments);
646+
};
647+
648+
if (fn) {
649+
this._imitate(wrapped, fn);
650+
fn.__raven_breadcrumb__ = wrapped;
651+
631652
}
653+
654+
return wrapped;
632655
},
633656

634657
/**
@@ -675,6 +698,13 @@ Raven.prototype = {
675698
});
676699
}
677700

701+
// Capture breadcrubms from any click that is unhandled / bubbled up all the way
702+
// to the document. Do this before we instrument addEventListener.
703+
if (this._hasDocument) {
704+
document.addEventListener('click', self._wrapEventHandlerForBreadcrumbs('click'));
705+
706+
}
707+
678708
// event targets borrowed from bugsnag-js:
679709
// https://github.com/bugsnag/bugsnag-js/blob/master/src/bugsnag.js#L666
680710
'EventTarget Window Node ApplicationCache AudioTrackList ChannelMergerNode CryptoOperation EventSource FileReader HTMLUnknownElement IDBDatabase IDBRequest IDBTransaction KeyOperation MediaController MessagePort ModalWindow Notification SVGElementInstance Screen TextTrack TextTrackCue TextTrackList WebSocket WebSocketWorker Worker XMLHttpRequest XMLHttpRequestEventTarget XMLHttpRequestUpload'.replace(/\w+/g, function (global) {
@@ -691,14 +721,18 @@ Raven.prototype = {
691721
}
692722

693723
// TODO: more than just click
694-
if (global === 'EventTarget' && evt === 'click') {
695-
fn = self._wrapEventHandlerForBreadcrumbs(this, evt, fn);
724+
if ((global === 'EventTarget' || global === 'Node') && evt === 'click') {
725+
fn = self._wrapEventHandlerForBreadcrumbs(evt, fn);
696726
}
697727
return orig.call(this, evt, self.wrap(fn), capture, secure);
698728
};
699729
});
700730
fill(proto, 'removeEventListener', function (orig) {
701731
return function (evt, fn, capture, secure) {
732+
// from the original function, get the breadcrumb wrapper
733+
// from the breadcrumb wrapper, get the raven wrapper (try/catch)
734+
// i.e. fn => breadcrumb_wrapper(fn) => raven_wrapper(breadcrumb_wrapper(fn))
735+
fn = fn && (fn.__raven_breadcrumb__ ? fn.__raven_breadcrumb__ : fn);
702736
fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn);
703737
return orig.call(this, evt, fn, capture, secure);
704738
};

test/integration/frame.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,13 @@
7373
</script>
7474
</head>
7575
<body>
76+
<!-- test element for breadcrumbs -->
77+
<input name="foo" id="bar" placeholder="lol"/>
78+
79+
<div id="c">
80+
<div id="b">
81+
<div id="a"/>
82+
</div>
83+
</div>
7684
</body>
7785
</html>

test/integration/test.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ describe('integration', function () {
173173
iframeExecute(iframe, done,
174174
function () {
175175
setTimeout(done);
176+
debugger;
176177

177178
var div = document.createElement('div');
178179
document.body.appendChild(div);
@@ -297,6 +298,145 @@ describe('integration', function () {
297298
}
298299
);
299300
});
301+
});
302+
303+
describe('breadcrumbs', function () {
304+
it('should record a mouse click on element WITH click handler present', function (done) {
305+
var iframe = this.iframe;
306+
307+
iframeExecute(iframe, done,
308+
function () {
309+
setTimeout(done);
310+
311+
// some browsers trigger onpopstate for load / reset breadcrumb state
312+
Raven._breadcrumbs = [];
313+
314+
// add an event listener to the input. we want to make sure that
315+
// our breadcrumbs still work even if the page has an event listener
316+
// on an element that cancels event bubbling
317+
var input = document.getElementsByTagName('input')[0];
318+
var clickHandler = function (evt) {
319+
evt.stopPropagation(); // don't bubble
320+
};
321+
input.addEventListener('click', clickHandler);
322+
323+
// click <input/>
324+
var evt = document.createEvent('MouseEvent');
325+
evt.initMouseEvent(
326+
"click",
327+
true /* bubble */,
328+
true /* cancelable */,
329+
window,
330+
null,
331+
0, 0, 0, 0, /* coordinates */
332+
false, false, false, false, /* modifier keys */
333+
0 /*left*/,
334+
null
335+
);
336+
input.dispatchEvent(evt);
337+
},
338+
function () {
339+
var Raven = iframe.contentWindow.Raven,
340+
breadcrumbs = Raven._breadcrumbs;
341+
342+
assert.equal(breadcrumbs.length, 1);
343+
344+
assert.equal(breadcrumbs[0].type, 'ui_event');
345+
// NOTE: attributes re-ordered. should this be expected?
346+
assert.equal(breadcrumbs[0].data.target, '<input id="bar" name="foo" placeholder="lol" />');
347+
assert.equal(breadcrumbs[0].data.type, 'click');
348+
}
349+
);
350+
});
351+
352+
it('should record a mouse click on element WITHOUT click handler present', function (done) {
353+
var iframe = this.iframe;
354+
355+
iframeExecute(iframe, done,
356+
function () {
357+
setTimeout(done);
358+
359+
// some browsers trigger onpopstate for load / reset breadcrumb state
360+
Raven._breadcrumbs = [];
361+
362+
// click <input/>
363+
var evt = document.createEvent('MouseEvent');
364+
evt.initMouseEvent(
365+
"click",
366+
true /* bubble */,
367+
true /* cancelable */,
368+
window,
369+
null,
370+
0, 0, 0, 0, /* coordinates */
371+
false, false, false, false, /* modifier keys */
372+
0 /*left*/,
373+
null
374+
);
375+
376+
var input = document.getElementsByTagName('input')[0];
377+
input.dispatchEvent(evt);
378+
},
379+
function () {
380+
var Raven = iframe.contentWindow.Raven,
381+
breadcrumbs = Raven._breadcrumbs;
382+
383+
assert.equal(breadcrumbs.length, 1);
384+
385+
assert.equal(breadcrumbs[0].type, 'ui_event');
386+
// NOTE: attributes re-ordered. should this be expected?
387+
assert.equal(breadcrumbs[0].data.target, '<input id="bar" name="foo" placeholder="lol" />');
388+
assert.equal(breadcrumbs[0].data.type, 'click');
389+
}
390+
);
391+
});
392+
393+
it('should only record a SINGLE mouse click for a tree of elements with event listeners', function (done) {
394+
var iframe = this.iframe;
395+
396+
iframeExecute(iframe, done,
397+
function () {
398+
setTimeout(done);
399+
400+
// some browsers trigger onpopstate for load / reset breadcrumb state
401+
Raven._breadcrumbs = [];
402+
403+
var clickHandler = function (evt) {
404+
//evt.stopPropagation();
405+
};
406+
document.getElementById('a').addEventListener('click', clickHandler);
407+
document.getElementById('b').addEventListener('click', clickHandler);
408+
document.getElementById('c').addEventListener('click', clickHandler);
409+
410+
// click <input/>
411+
var evt = document.createEvent('MouseEvent');
412+
evt.initMouseEvent(
413+
"click",
414+
true /* bubble */,
415+
true /* cancelable */,
416+
window,
417+
null,
418+
0, 0, 0, 0, /* coordinates */
419+
false, false, false, false, /* modifier keys */
420+
0 /*left*/,
421+
null
422+
);
423+
424+
var input = document.getElementById('a'); // leaf node
425+
input.dispatchEvent(evt);
426+
},
427+
function () {
428+
var Raven = iframe.contentWindow.Raven,
429+
breadcrumbs = Raven._breadcrumbs;
430+
431+
assert.equal(breadcrumbs.length, 1);
432+
433+
assert.equal(breadcrumbs[0].type, 'ui_event');
434+
// NOTE: attributes re-ordered. should this be expected?
435+
assert.equal(breadcrumbs[0].data.target, '<div id="a" />');
436+
assert.equal(breadcrumbs[0].data.type, 'click');
437+
}
438+
);
439+
});
300440

301441
it('should record history.[pushState|back] changes as navigation breadcrumbs', function (done) {
302442
var iframe = this.iframe;

0 commit comments

Comments
 (0)