Skip to content

Commit 4ac9f5b

Browse files
committed
Error OR ui click now flushes debounced keypress handler, more ...
* Keypress events only considered on INPUT or TEXTAREA elems * Remove generic debounce util method * Tests
1 parent 32e4d83 commit 4ac9f5b

File tree

3 files changed

+205
-25
lines changed

3 files changed

+205
-25
lines changed

src/raven.js

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ var urlencode = utils.urlencode;
1919
var uuid4 = utils.uuid4;
2020
var htmlTreeAsString = utils.htmlTreeAsString;
2121
var parseUrl = utils.parseUrl;
22-
var debounce = utils.debounce;
2322

2423
var dsnKeys = 'source protocol user pass host port path'.split(' '),
2524
dsnPattern = /^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/;
@@ -65,6 +64,7 @@ function Raven() {
6564
this._breadcrumbs = [];
6665
this._breadcrumbLimit = 20;
6766
this._lastCapturedEvent = null;
67+
this._keypressTimeout = null;
6868
this._location = window.location;
6969
this._lastHref = this._location && this._location.href;
7070

@@ -629,6 +629,13 @@ Raven.prototype = {
629629
_breadcrumbEventHandler: function(evtName) {
630630
var self = this;
631631
return function (evt) {
632+
// if there's a keypress event still queued up (debounce
633+
// hasn't flushed yet), flush it immediately before processing
634+
// this event
635+
if (self._keypressTimeout) {
636+
self._keypressCapture();
637+
}
638+
632639
// It's possible this handler might trigger multiple times for the same
633640
// event (e.g. event propagation through node ancestors). Ignore if we've
634641
// already captured the event.
@@ -655,15 +662,42 @@ Raven.prototype = {
655662
};
656663
},
657664

665+
_keypressCapture: function(evt) {
666+
evt = evt || this._lastKeypressEvent;
667+
this._lastKeypressEvent = null;
668+
669+
if (this._keypressTimeout) {
670+
clearTimeout(this._keypressTimeout);
671+
this._keypressTimeout = null;
672+
}
673+
674+
return this._breadcrumbEventHandler('input')(evt);
675+
},
676+
658677
_keypressEventHandler: function() {
659678
var self = this;
679+
var debounceDuration = 1000; // milliseconds
660680

661681
// TODO: if somehow user switches keypress target before
662682
// debounce timeout is triggered, we will only capture
663683
// a single breadcrumb from the LAST target (acceptable?)
664-
return debounce(function (evt) {
665-
self._breadcrumbEventHandler('keypress')(evt);
666-
}, 500); // 500ms after last consecutive keypress, record breadcrumb
684+
685+
return function (evt) {
686+
var target = evt.target,
687+
tagName = target && target.tagName;
688+
689+
// only consider keypress events on actual input elements
690+
// this will disregard keypresses targeting body (e.g. tabbing
691+
// through elements, hotkeys, etc)
692+
if (!tagName || tagName !== 'INPUT' && tagName !== 'TEXTAREA')
693+
return;
694+
695+
clearTimeout(self._keypressTimeout);
696+
self._lastKeypressEvent = evt;
697+
self._keypressTimeout = setTimeout(function () {
698+
self._keypressCapture(evt);
699+
}, debounceDuration);
700+
};
667701
},
668702

669703
/**
@@ -1167,6 +1201,10 @@ Raven.prototype = {
11671201
// Send along our own collected metadata with extra
11681202
data.extra['session:duration'] = now() - this._startTime;
11691203

1204+
// flush debounced keypress breadcrumb capture (if there is one)
1205+
if (this._keypressTimeout) {
1206+
this._keypressCapture();
1207+
}
11701208
if (this._breadcrumbs && this._breadcrumbs.length > 0) {
11711209
// intentionally make shallow copy so that additions
11721210
// to breadcrumbs aren't accidentally sent in this request

src/utils.js

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -153,26 +153,6 @@ function uuid4() {
153153
}
154154
}
155155

156-
// Returns a function, that, as long as it continues to be invoked, will not
157-
// be triggered. The function will be called after it stops being called for
158-
// N milliseconds. If `immediate` is passed, trigger the function on the
159-
// leading edge, instead of the trailing.
160-
// https://davidwalsh.name/javascript-debounce-function
161-
function debounce(func, wait) {
162-
var timeout;
163-
return function() {
164-
var context = this,
165-
args = arguments;
166-
167-
var later = function() {
168-
timeout = null;
169-
func.apply(context, args);
170-
};
171-
clearTimeout(timeout);
172-
timeout = setTimeout(later, wait);
173-
};
174-
};
175-
176156
/**
177157
* Given a child DOM element, returns a query-selector statement describing that
178158
* and its ancestors
@@ -265,7 +245,6 @@ module.exports = {
265245
joinRegExp: joinRegExp,
266246
urlencode: urlencode,
267247
uuid4: uuid4,
268-
debounce: debounce,
269248
htmlTreeAsString: htmlTreeAsString,
270249
htmlElementAsString: htmlElementAsString,
271250
parseUrl: parseUrl

test/integration/test.js

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,169 @@ describe('integration', function () {
536536
);
537537
});
538538

539+
it('should record consecutive keypress events into a single "input" breadcrumb', function (done) {
540+
var iframe = this.iframe;
541+
542+
iframeExecute(iframe, done,
543+
function () {
544+
// keypress events are debounced 1000ms - wait until
545+
// the debounce finishes
546+
setTimeout(done, 1001);
547+
548+
// some browsers trigger onpopstate for load / reset breadcrumb state
549+
Raven._breadcrumbs = [];
550+
551+
// keypress <input/> twice
552+
var keypress1 = document.createEvent('MouseEvent');
553+
keypress1.initMouseEvent(
554+
"keypress",
555+
true /* bubble */,
556+
true /* cancelable */,
557+
window,
558+
null,
559+
0, 0, 0, 0, /* coordinates */
560+
false, false, false, false, /* modifier keys */
561+
0 /*left*/,
562+
null
563+
);
564+
565+
var keypress2 = document.createEvent('MouseEvent');
566+
keypress2.initMouseEvent(
567+
"keypress",
568+
true /* bubble */,
569+
true /* cancelable */,
570+
window,
571+
null,
572+
0, 0, 0, 0, /* coordinates */
573+
false, false, false, false, /* modifier keys */
574+
0 /*left*/,
575+
null
576+
);
577+
578+
var input = document.getElementsByTagName('input')[0];
579+
input.dispatchEvent(keypress1);
580+
input.dispatchEvent(keypress2);
581+
},
582+
function () {
583+
var Raven = iframe.contentWindow.Raven,
584+
breadcrumbs = Raven._breadcrumbs;
585+
586+
assert.equal(breadcrumbs.length, 1);
587+
588+
assert.equal(breadcrumbs[0].type, 'ui_event');
589+
// NOTE: attributes re-ordered. should this be expected?
590+
assert.equal(breadcrumbs[0].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
591+
assert.equal(breadcrumbs[0].data.type, 'input');
592+
}
593+
);
594+
});
595+
596+
it('should flush keypress breadcrumbs when an error is thrown', function (done) {
597+
var iframe = this.iframe;
598+
599+
iframeExecute(iframe, done,
600+
function () {
601+
setTimeout(done);
602+
603+
// some browsers trigger onpopstate for load / reset breadcrumb state
604+
Raven._breadcrumbs = [];
605+
606+
// click <input/>
607+
var evt = document.createEvent('MouseEvent');
608+
evt.initMouseEvent(
609+
"keypress",
610+
true /* bubble */,
611+
true /* cancelable */,
612+
window,
613+
null,
614+
0, 0, 0, 0, /* coordinates */
615+
false, false, false, false, /* modifier keys */
616+
0 /*left*/,
617+
null
618+
);
619+
620+
var input = document.getElementsByTagName('input')[0];
621+
input.dispatchEvent(evt);
622+
623+
foo(); // throw exception
624+
},
625+
function () {
626+
var Raven = iframe.contentWindow.Raven,
627+
breadcrumbs = Raven._breadcrumbs;
628+
629+
// 2 breadcrumbs: `ui_event`, then `error`
630+
assert.equal(breadcrumbs.length, 2);
631+
632+
assert.equal(breadcrumbs[0].type, 'ui_event');
633+
// NOTE: attributes re-ordered. should this be expected?
634+
assert.equal(breadcrumbs[0].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
635+
assert.equal(breadcrumbs[0].data.type, 'input');
636+
}
637+
);
638+
});
639+
640+
it('should flush keypress breadcrumb when input event occurs immediately after', function (done) {
641+
var iframe = this.iframe;
642+
643+
iframeExecute(iframe, done,
644+
function () {
645+
setTimeout(done);
646+
647+
// some browsers trigger onpopstate for load / reset breadcrumb state
648+
Raven._breadcrumbs = [];
649+
650+
// keypress <input/>
651+
var keypressEvent = document.createEvent('MouseEvent');
652+
keypressEvent.initMouseEvent(
653+
"keypress",
654+
true /* bubble */,
655+
true /* cancelable */,
656+
window,
657+
null,
658+
0, 0, 0, 0, /* coordinates */
659+
false, false, false, false, /* modifier keys */
660+
0 /*left*/,
661+
null
662+
);
663+
664+
// click <input/>
665+
var clickEvent = document.createEvent('MouseEvent');
666+
clickEvent.initMouseEvent(
667+
"click",
668+
true /* bubble */,
669+
true /* cancelable */,
670+
window,
671+
null,
672+
0, 0, 0, 0, /* coordinates */
673+
false, false, false, false, /* modifier keys */
674+
0 /*left*/,
675+
null
676+
);
677+
678+
var input = document.getElementsByTagName('input')[0];
679+
input.dispatchEvent(keypressEvent);
680+
input.dispatchEvent(clickEvent);
681+
},
682+
function () {
683+
var Raven = iframe.contentWindow.Raven,
684+
breadcrumbs = Raven._breadcrumbs;
685+
686+
// 2x `ui_event`
687+
assert.equal(breadcrumbs.length, 2);
688+
689+
assert.equal(breadcrumbs[0].type, 'ui_event');
690+
// NOTE: attributes re-ordered. should this be expected?
691+
assert.equal(breadcrumbs[0].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
692+
assert.equal(breadcrumbs[0].data.type, 'input');
693+
694+
assert.equal(breadcrumbs[1].type, 'ui_event');
695+
// NOTE: attributes re-ordered. should this be expected?
696+
assert.equal(breadcrumbs[1].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
697+
assert.equal(breadcrumbs[1].data.type, 'click');
698+
}
699+
);
700+
});
701+
539702
it('should record history.[pushState|back] changes as navigation breadcrumbs', function (done) {
540703
var iframe = this.iframe;
541704

0 commit comments

Comments
 (0)