Skip to content

Commit f3a70ee

Browse files
committed
Merge pull request #562 from getsentry/keypress
Collect consecutive keypresses on input elems as "input" breadcrumb
2 parents 521768a + b54d20f commit f3a70ee

File tree

3 files changed

+192
-11
lines changed

3 files changed

+192
-11
lines changed

example/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<title>Scratch Disk</title>
55
</head>
6-
<script src="../dist/raven.js"></script>
6+
<script src="../build/raven.js"></script>
77
<!-- <script src="scratch.min.js"></script> -->
88
<script src="scratch.js" crossorigin></script>
99
<script src="file.min.js" crossorigin></script>
@@ -39,6 +39,7 @@
3939
<button onclick="throwString()">throw string</button>
4040
<button onclick="showDialog()">show dialog</button>
4141
<button onclick="blobExample()">blob example</button>
42+
<input/>
4243

4344
</body>
4445
</html>

src/raven.js

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function Raven() {
6464
this._breadcrumbs = [];
6565
this._breadcrumbLimit = 20;
6666
this._lastCapturedEvent = null;
67+
this._keypressTimeout;
6768
this._location = window.location;
6869
this._lastHref = this._location && this._location.href;
6970

@@ -618,17 +619,20 @@ Raven.prototype = {
618619
}
619620
},
620621

621-
622622
/**
623623
* Wraps addEventListener to capture UI breadcrumbs
624624
* @param evtName the event name (e.g. "click")
625-
* @param fn the function being wrapped
626625
* @returns {Function}
627626
* @private
628627
*/
629628
_breadcrumbEventHandler: function(evtName) {
630629
var self = this;
631630
return function (evt) {
631+
// reset keypress timeout; e.g. triggering a 'click' after
632+
// a 'keypress' will reset the keypress debounce so that a new
633+
// set of keypresses can be recorded
634+
self._keypressTimeout = null;
635+
632636
// It's possible this handler might trigger multiple times for the same
633637
// event (e.g. event propagation through node ancestors). Ignore if we've
634638
// already captured the event.
@@ -637,13 +641,60 @@ Raven.prototype = {
637641

638642
self._lastCapturedEvent = evt;
639643
var elem = evt.target;
644+
645+
var target;
646+
647+
// try/catch htmlTreeAsString because it's particularly complicated, and
648+
// just accessing the DOM incorrectly can throw an exception in some circumstances.
649+
try {
650+
target = htmlTreeAsString(elem);
651+
} catch (e) {
652+
target = '<unknown>';
653+
}
654+
640655
self.captureBreadcrumb('ui_event', {
641656
type: evtName,
642-
target: htmlTreeAsString(elem)
657+
target: target
643658
});
644659
};
645660
},
646661

662+
/**
663+
* Wraps addEventListener to capture keypress UI events
664+
* @returns {Function}
665+
* @private
666+
*/
667+
_keypressEventHandler: function() {
668+
var self = this,
669+
debounceDuration = 1000; // milliseconds
670+
671+
// TODO: if somehow user switches keypress target before
672+
// debounce timeout is triggered, we will only capture
673+
// a single breadcrumb from the FIRST target (acceptable?)
674+
675+
return function (evt) {
676+
var target = evt.target,
677+
tagName = target && target.tagName;
678+
679+
// only consider keypress events on actual input elements
680+
// this will disregard keypresses targeting body (e.g. tabbing
681+
// through elements, hotkeys, etc)
682+
if (!tagName || tagName !== 'INPUT' && tagName !== 'TEXTAREA')
683+
return;
684+
685+
// record first keypress in a series, but ignore subsequent
686+
// keypresses until debounce clears
687+
var timeout = self._keypressTimeout;
688+
if (!timeout) {
689+
self._breadcrumbEventHandler('input')(evt);
690+
}
691+
clearTimeout(timeout);
692+
self._keypressTimeout = setTimeout(function () {
693+
self._keypressTimeout = null;
694+
}, debounceDuration);
695+
};
696+
},
697+
647698
/**
648699
* Captures a breadcrumb of type "navigation", normalizing input URLs
649700
* @param to the originating URL
@@ -715,7 +766,7 @@ Raven.prototype = {
715766
var proto = window[global] && window[global].prototype;
716767
if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) {
717768
fill(proto, 'addEventListener', function(orig) {
718-
return function (evt, fn, capture, secure) { // preserve arity
769+
return function (evtName, fn, capture, secure) { // preserve arity
719770
try {
720771
if (fn && fn.handleEvent) {
721772
fn.handleEvent = self.wrap(fn.handleEvent);
@@ -727,10 +778,14 @@ Raven.prototype = {
727778

728779
// TODO: more than just click
729780
var before;
730-
if ((global === 'EventTarget' || global === 'Node') && evt === 'click') {
731-
before = self._breadcrumbEventHandler(evt, fn);
781+
if (global === 'EventTarget' || global === 'Node') {
782+
if (evtName === 'click'){
783+
before = self._breadcrumbEventHandler(evtName);
784+
} else if (evtName === 'keypress') {
785+
before = self._keypressEventHandler();
786+
}
732787
}
733-
return orig.call(this, evt, self.wrap(fn, undefined, before), capture, secure);
788+
return orig.call(this, evtName, self.wrap(fn, undefined, before), capture, secure);
734789
};
735790
});
736791
fill(proto, 'removeEventListener', function (orig) {
@@ -764,6 +819,7 @@ Raven.prototype = {
764819
// to the document. Do this before we instrument addEventListener.
765820
if (this._hasDocument) {
766821
document.addEventListener('click', self._breadcrumbEventHandler('click'));
822+
document.addEventListener('keypress', self._keypressEventHandler());
767823
}
768824

769825
// event targets borrowed from bugsnag-js:

test/integration/test.js

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,6 @@ describe('integration', function () {
440440
assert.equal(breadcrumbs.length, 1);
441441

442442
assert.equal(breadcrumbs[0].type, 'ui_event');
443-
// NOTE: attributes re-ordered. should this be expected?
444443
assert.equal(breadcrumbs[0].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
445444
assert.equal(breadcrumbs[0].data.type, 'click');
446445
}
@@ -481,7 +480,6 @@ describe('integration', function () {
481480
assert.equal(breadcrumbs.length, 1);
482481

483482
assert.equal(breadcrumbs[0].type, 'ui_event');
484-
// NOTE: attributes re-ordered. should this be expected?
485483
assert.equal(breadcrumbs[0].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
486484
assert.equal(breadcrumbs[0].data.type, 'click');
487485
}
@@ -529,13 +527,139 @@ describe('integration', function () {
529527
assert.equal(breadcrumbs.length, 1);
530528

531529
assert.equal(breadcrumbs[0].type, 'ui_event');
532-
// NOTE: attributes re-ordered. should this be expected?
533530
assert.equal(breadcrumbs[0].data.target, 'body > div.c > div.b > div.a');
534531
assert.equal(breadcrumbs[0].data.type, 'click');
535532
}
536533
);
537534
});
538535

536+
it('should record consecutive keypress events into a single "input" breadcrumb', function (done) {
537+
var iframe = this.iframe;
538+
539+
iframeExecute(iframe, done,
540+
function () {
541+
setTimeout(done);
542+
543+
// some browsers trigger onpopstate for load / reset breadcrumb state
544+
Raven._breadcrumbs = [];
545+
546+
// keypress <input/> twice
547+
var keypress1 = document.createEvent('KeyboardEvent');
548+
keypress1.initKeyboardEvent("keypress", true, true, window, "b", 66, 0, "", false);
549+
550+
var keypress2 = document.createEvent('KeyboardEvent');
551+
keypress2.initKeyboardEvent("keypress", true, true, window, "a", 65, 0, "", false);
552+
553+
var input = document.getElementsByTagName('input')[0];
554+
input.dispatchEvent(keypress1);
555+
input.dispatchEvent(keypress2);
556+
},
557+
function () {
558+
var Raven = iframe.contentWindow.Raven,
559+
breadcrumbs = Raven._breadcrumbs;
560+
561+
assert.equal(breadcrumbs.length, 1);
562+
563+
assert.equal(breadcrumbs[0].type, 'ui_event');
564+
assert.equal(breadcrumbs[0].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
565+
assert.equal(breadcrumbs[0].data.type, 'input');
566+
}
567+
);
568+
});
569+
570+
it('should flush keypress breadcrumbs when an error is thrown', function (done) {
571+
var iframe = this.iframe;
572+
573+
iframeExecute(iframe, done,
574+
function () {
575+
setTimeout(done);
576+
577+
// some browsers trigger onpopstate for load / reset breadcrumb state
578+
Raven._breadcrumbs = [];
579+
580+
// keypress <input/>
581+
var keypress = document.createEvent('KeyboardEvent');
582+
keypress.initKeyboardEvent("keypress", true, true, window, "b", 66, 0, "", false);
583+
584+
var input = document.getElementsByTagName('input')[0];
585+
input.dispatchEvent(keypress);
586+
587+
foo(); // throw exception
588+
},
589+
function () {
590+
var Raven = iframe.contentWindow.Raven,
591+
breadcrumbs = Raven._breadcrumbs;
592+
593+
// 2 breadcrumbs: `ui_event`, then `error`
594+
assert.equal(breadcrumbs.length, 2);
595+
596+
assert.equal(breadcrumbs[0].type, 'ui_event');
597+
assert.equal(breadcrumbs[0].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
598+
assert.equal(breadcrumbs[0].data.type, 'input');
599+
}
600+
);
601+
});
602+
603+
it('should flush keypress breadcrumb when input event occurs immediately after', function (done) {
604+
var iframe = this.iframe;
605+
606+
iframeExecute(iframe, done,
607+
function () {
608+
setTimeout(done);
609+
610+
// some browsers trigger onpopstate for load / reset breadcrumb state
611+
Raven._breadcrumbs = [];
612+
613+
// 1st keypress <input/>
614+
var keypress1 = document.createEvent('KeyboardEvent');
615+
keypress1.initKeyboardEvent("keypress", true, true, window, "b", 66, 0, "", false);
616+
617+
// click <input/>
618+
var click= document.createEvent('MouseEvent');
619+
click.initMouseEvent(
620+
"click",
621+
true /* bubble */,
622+
true /* cancelable */,
623+
window,
624+
null,
625+
0, 0, 0, 0, /* coordinates */
626+
false, false, false, false, /* modifier keys */
627+
0 /*left*/,
628+
null
629+
);
630+
631+
// 2nd keypress
632+
var keypress2 = document.createEvent('KeyboardEvent');
633+
keypress2.initKeyboardEvent("keypress", true, true, window, "a", 65, 0, "", false);
634+
635+
var input = document.getElementsByTagName('input')[0];
636+
input.dispatchEvent(keypress1);
637+
input.dispatchEvent(click);
638+
input.dispatchEvent(keypress2);
639+
},
640+
function () {
641+
var Raven = iframe.contentWindow.Raven,
642+
breadcrumbs = Raven._breadcrumbs;
643+
644+
// 2x `ui_event`
645+
assert.equal(breadcrumbs.length, 3);
646+
647+
assert.equal(breadcrumbs[0].type, 'ui_event');
648+
assert.equal(breadcrumbs[0].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
649+
assert.equal(breadcrumbs[0].data.type, 'input');
650+
651+
assert.equal(breadcrumbs[1].type, 'ui_event');
652+
assert.equal(breadcrumbs[1].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
653+
assert.equal(breadcrumbs[1].data.type, 'click');
654+
655+
assert.equal(breadcrumbs[2].type, 'ui_event');
656+
assert.equal(breadcrumbs[2].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
657+
assert.equal(breadcrumbs[2].data.type, 'input');
658+
659+
}
660+
);
661+
});
662+
539663
it('should record history.[pushState|back] changes as navigation breadcrumbs', function (done) {
540664
var iframe = this.iframe;
541665

0 commit comments

Comments
 (0)