Skip to content

Commit ebdc38a

Browse files
committed
Merge pull request #559 from getsentry/show-click-ancestors
Show ancestor query selectors in click target breadcrumbs
2 parents c49926e + 6c0eb92 commit ebdc38a

File tree

5 files changed

+121
-19
lines changed

5 files changed

+121
-19
lines changed

src/raven.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ var objectMerge = utils.objectMerge;
1717
var truncate = utils.truncate;
1818
var urlencode = utils.urlencode;
1919
var uuid4 = utils.uuid4;
20-
var htmlElementAsString = utils.htmlElementAsString;
20+
var htmlTreeAsString = utils.htmlTreeAsString;
2121
var parseUrl = utils.parseUrl;
2222

2323
var dsnKeys = 'source protocol user pass host port path'.split(' '),
@@ -639,7 +639,7 @@ Raven.prototype = {
639639
var elem = evt.target;
640640
self.captureBreadcrumb('ui_event', {
641641
type: evtName,
642-
target: htmlElementAsString(elem)
642+
target: htmlTreeAsString(elem)
643643
});
644644
};
645645
},

src/utils.js

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/*eslint no-extra-parens:0*/
12
'use strict';
23

34
var objectPrototype = Object.prototype;
@@ -152,32 +153,75 @@ function uuid4() {
152153
}
153154
}
154155

156+
/**
157+
* Given a child DOM element, returns a query-selector statement describing that
158+
* and its ancestors
159+
* e.g. [HTMLElement] => body > div > input#foo.btn[name=baz]
160+
* @param elem
161+
* @returns {string}
162+
*/
163+
function htmlTreeAsString(elem) {
164+
var MAX_TRAVERSE_HEIGHT = 5,
165+
MAX_OUTPUT_LEN = 80,
166+
out = [],
167+
height = 0,
168+
len = 0,
169+
separator = ' > ',
170+
sepLength = separator.length,
171+
nextStr;
172+
173+
while (elem && height++ < MAX_TRAVERSE_HEIGHT) {
174+
175+
nextStr = htmlElementAsString(elem);
176+
// bail out if
177+
// - nextStr is the 'html' element
178+
// - the length of the string that would be created exceeds MAX_OUTPUT_LEN
179+
// (ignore this limit if we are on the first iteration)
180+
if (nextStr === 'html' || height > 1 && len + (out.length * sepLength) + nextStr.length >= MAX_OUTPUT_LEN) {
181+
break;
182+
}
183+
184+
out.push(nextStr);
185+
186+
len += nextStr.length;
187+
elem = elem.parentNode;
188+
}
189+
190+
return out.reverse().join(separator);
191+
}
192+
155193
/**
156194
* Returns a simple, query-selector representation of a DOM element
157195
* e.g. [HTMLElement] => input#foo.btn[name=baz]
158196
* @param HTMLElement
197+
* @returns {string}
159198
*/
160199
function htmlElementAsString(elem) {
161-
var out = [];
162-
out.push(elem.tagName.toLowerCase());
200+
var out = [],
201+
classes,
202+
key,
203+
attr,
204+
i;
163205

206+
out.push(elem.tagName.toLowerCase());
164207
if (elem.id) {
165208
out.push('#' + elem.id);
166209
}
167-
var classes, i;
210+
168211
if (elem.className) {
169212
classes = elem.className.split(' ');
170213
for (i = 0; i < classes.length; i++) {
171214
out.push('.' + classes[i]);
172215
}
173216
}
174217
var attrWhitelist = ['type', 'name', 'value', 'placeholder', 'title', 'alt'];
175-
each(attrWhitelist, function(index, key) {
176-
var attr = elem.getAttribute(key);
218+
for (i = 0; i < attrWhitelist.length; i++) {
219+
key = attrWhitelist[i];
220+
attr = elem.getAttribute(key);
177221
if (attr) {
178222
out.push('[' + key + '="' + attr + '"]');
179223
}
180-
});
224+
}
181225
return out.join('');
182226
}
183227

@@ -195,6 +239,7 @@ module.exports = {
195239
joinRegExp: joinRegExp,
196240
urlencode: urlencode,
197241
uuid4: uuid4,
242+
htmlTreeAsString: htmlTreeAsString,
198243
htmlElementAsString: htmlElementAsString,
199244
parseUrl: parseUrl
200245
};

test/integration/frame.html

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,13 @@
7474
</head>
7575
<body>
7676
<!-- test element for breadcrumbs -->
77-
<input name="foo" id="bar" placeholder="lol"/>
77+
<form id="foo-form">
78+
<input name="foo" placeholder="lol"/>
79+
</form>
7880

79-
<div id="c">
80-
<div id="b">
81-
<div id="a"/>
81+
<div class="c">
82+
<div class="b">
83+
<div class="a"/>
8284
</div>
8385
</div>
8486
</body>

test/integration/test.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ describe('integration', function () {
441441

442442
assert.equal(breadcrumbs[0].type, 'ui_event');
443443
// NOTE: attributes re-ordered. should this be expected?
444-
assert.equal(breadcrumbs[0].data.target, 'input#bar[name="foo"][placeholder="lol"]');
444+
assert.equal(breadcrumbs[0].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
445445
assert.equal(breadcrumbs[0].data.type, 'click');
446446
}
447447
);
@@ -482,7 +482,7 @@ describe('integration', function () {
482482

483483
assert.equal(breadcrumbs[0].type, 'ui_event');
484484
// NOTE: attributes re-ordered. should this be expected?
485-
assert.equal(breadcrumbs[0].data.target, 'input#bar[name="foo"][placeholder="lol"]');
485+
assert.equal(breadcrumbs[0].data.target, 'body > form#foo-form > input[name="foo"][placeholder="lol"]');
486486
assert.equal(breadcrumbs[0].data.type, 'click');
487487
}
488488
);
@@ -501,9 +501,9 @@ describe('integration', function () {
501501
var clickHandler = function (evt) {
502502
//evt.stopPropagation();
503503
};
504-
document.getElementById('a').addEventListener('click', clickHandler);
505-
document.getElementById('b').addEventListener('click', clickHandler);
506-
document.getElementById('c').addEventListener('click', clickHandler);
504+
document.querySelector('.a').addEventListener('click', clickHandler);
505+
document.querySelector('.b').addEventListener('click', clickHandler);
506+
document.querySelector('.c').addEventListener('click', clickHandler);
507507

508508
// click <input/>
509509
var evt = document.createEvent('MouseEvent');
@@ -519,7 +519,7 @@ describe('integration', function () {
519519
null
520520
);
521521

522-
var input = document.getElementById('a'); // leaf node
522+
var input = document.querySelector('.a'); // leaf node
523523
input.dispatchEvent(evt);
524524
},
525525
function () {
@@ -530,7 +530,7 @@ describe('integration', function () {
530530

531531
assert.equal(breadcrumbs[0].type, 'ui_event');
532532
// NOTE: attributes re-ordered. should this be expected?
533-
assert.equal(breadcrumbs[0].data.target, 'div#a');
533+
assert.equal(breadcrumbs[0].data.target, 'body > div.c > div.b > div.a');
534534
assert.equal(breadcrumbs[0].data.type, 'click');
535535
}
536536
);

test/utils.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var joinRegExp = utils.joinRegExp;
1616
var objectMerge = utils.objectMerge;
1717
var truncate = utils.truncate;
1818
var urlencode = utils.urlencode;
19+
var htmlTreeAsString = utils.htmlTreeAsString;
1920
var htmlElementAsString = utils.htmlElementAsString;
2021
var parseUrl = utils.parseUrl;
2122

@@ -121,6 +122,60 @@ describe('utils', function () {
121122
});
122123
});
123124

125+
describe('htmlTreeAsString', function () {
126+
it('should work', function () {
127+
var tree = {
128+
tagName: 'INPUT',
129+
id: 'the-username',
130+
className: 'form-control',
131+
getAttribute: function (key){
132+
return {
133+
name: 'username'
134+
}[key];
135+
},
136+
parentNode: {
137+
tagName: 'span',
138+
getAttribute: function () {},
139+
parentNode: {
140+
tagName: 'div',
141+
getAttribute: function () {},
142+
}
143+
}
144+
};
145+
146+
assert.equal(htmlTreeAsString(tree), 'div > span > input#the-username.form-control[name="username"]');
147+
});
148+
149+
150+
it('should not create strings that are too big', function () {
151+
var tree = {
152+
tagName: 'INPUT',
153+
id: 'the-username',
154+
className: 'form-control',
155+
getAttribute: function (key){
156+
return {
157+
name: 'username'
158+
}[key];
159+
},
160+
parentNode: {
161+
tagName: 'span',
162+
getAttribute: function () {},
163+
parentNode: {
164+
tagName: 'div',
165+
getAttribute: function (key) {
166+
return {
167+
name: 'super long input name that nobody would really ever have i mean come on look at this'
168+
}[key];
169+
}
170+
}
171+
}
172+
};
173+
174+
// NOTE: <div/> omitted because crazy long name
175+
assert.equal(htmlTreeAsString(tree), 'span > input#the-username.form-control[name="username"]');
176+
});
177+
});
178+
124179
describe('htmlElementAsString', function () {
125180
it('should work', function () {
126181
assert.equal(htmlElementAsString({

0 commit comments

Comments
 (0)