Skip to content

Commit 55714fe

Browse files
committed
feat(core): Add data-sentry-label support in htmlTreeAsString
1 parent f961771 commit 55714fe

File tree

2 files changed

+119
-4
lines changed

2 files changed

+119
-4
lines changed

packages/core/src/utils/browser.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ type SimpleNode = {
1111

1212
/**
1313
* Given a child DOM element, returns a query-selector statement describing that
14-
* and its ancestors
14+
* and its ancestors, optionally prefixed with data-sentry-label if found on an ancestor
1515
* e.g. [HTMLElement] => body > div > input#foo.btn[name=baz]
16+
* e.g. [HTMLElement] => [data-sentry-label="MyLabel"] div.container > button
1617
* @returns generated DOM path
1718
*/
1819
export function htmlTreeAsString(
@@ -30,7 +31,7 @@ export function htmlTreeAsString(
3031
try {
3132
let currentElem = elem as SimpleNode;
3233
const MAX_TRAVERSE_HEIGHT = 5;
33-
const out = [];
34+
const out: string[] = [];
3435
let height = 0;
3536
let len = 0;
3637
const separator = ' > ';
@@ -55,7 +56,18 @@ export function htmlTreeAsString(
5556
currentElem = currentElem.parentNode;
5657
}
5758

58-
return out.reverse().join(separator);
59+
const cssSelector = out.reverse().join(separator);
60+
61+
if (cssSelector.includes('[data-sentry-label="')) {
62+
return cssSelector;
63+
}
64+
65+
const sentryLabel = _getSentryLabel(elem);
66+
if (sentryLabel) {
67+
return `[data-sentry-label="${sentryLabel}"] ${cssSelector}`;
68+
}
69+
70+
return cssSelector;
5971
} catch {
6072
return '<unknown>';
6173
}
@@ -84,6 +96,9 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string {
8496
if (WINDOW.HTMLElement) {
8597
// If using the component name annotation plugin, this value may be available on the DOM node
8698
if (elem instanceof HTMLElement && elem.dataset) {
99+
if (elem.dataset['sentryLabel']) {
100+
return `[data-sentry-label="${elem.dataset['sentryLabel']}"]`;
101+
}
87102
if (elem.dataset['sentryComponent']) {
88103
return elem.dataset['sentryComponent'];
89104
}
@@ -128,6 +143,27 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string {
128143
return out.join('');
129144
}
130145

146+
/**
147+
* Searches for the data-sentry-label attribute up the DOM tree.
148+
* @returns The value of the first data-sentry-label found, or null if not found
149+
*/
150+
function _getSentryLabel(elem: unknown): string | null {
151+
const MAX_LABEL_TRAVERSE_HEIGHT = 15;
152+
let labelElem = elem as SimpleNode;
153+
154+
for (let i = 0; i < MAX_LABEL_TRAVERSE_HEIGHT && labelElem; i++) {
155+
// @ts-expect-error WINDOW has HTMLElement
156+
if (WINDOW.HTMLElement && labelElem instanceof HTMLElement && labelElem.dataset) {
157+
if (labelElem.dataset['sentryLabel']) {
158+
return labelElem.dataset['sentryLabel'];
159+
}
160+
}
161+
labelElem = labelElem.parentNode;
162+
}
163+
164+
return null;
165+
}
166+
131167
/**
132168
* A safe form of location.href
133169
*/

packages/core/test/lib/utils/browser.test.ts

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { htmlTreeAsString } from '../../../src/utils/browser';
55
beforeAll(() => {
66
const dom = new JSDOM();
77
global.document = dom.window.document;
8-
global.HTMLElement = new JSDOM().window.HTMLElement;
8+
global.HTMLElement = dom.window.HTMLElement;
99
});
1010

1111
describe('htmlTreeAsString', () => {
@@ -73,4 +73,83 @@ describe('htmlTreeAsString', () => {
7373
'div#main-cta > div.container > button.bg-blue-500.hover:bg-blue-700.text-white.hover:text-blue-100',
7474
);
7575
});
76+
77+
describe('data-sentry-label support', () => {
78+
it('returns data-sentry-label when element has the attribute directly', () => {
79+
const el = document.createElement('div');
80+
el.innerHTML = '<button data-sentry-label="SubmitButton" class="btn" />';
81+
document.body.appendChild(el);
82+
83+
expect(htmlTreeAsString(document.querySelector('button'))).toBe(
84+
'body > div > [data-sentry-label="SubmitButton"]',
85+
);
86+
});
87+
88+
it('includes data-sentry-label from ancestor element in the path', () => {
89+
const el = document.createElement('div');
90+
el.innerHTML = `<div data-sentry-label="LoginForm">
91+
<div class="form-group">
92+
<button id="submit-btn" class="btn" />
93+
</div>
94+
</div>`;
95+
document.body.appendChild(el);
96+
97+
expect(htmlTreeAsString(document.getElementById('submit-btn'))).toBe(
98+
'div > [data-sentry-label="LoginForm"] > div.form-group > button#submit-btn.btn',
99+
);
100+
});
101+
102+
it('finds data-sentry-label on a distant ancestor within traverse limit', () => {
103+
const el = document.createElement('div');
104+
el.innerHTML = `<div data-sentry-label="DeepForm">
105+
<div class="level-1">
106+
<div class="level-2">
107+
<div class="level-3">
108+
<div class="level-4">
109+
<div class="level-5">
110+
<button id="deep-btn" />
111+
</div>
112+
</div>
113+
</div>
114+
</div>
115+
</div>
116+
</div>`;
117+
document.body.appendChild(el);
118+
119+
const result = htmlTreeAsString(document.getElementById('deep-btn'));
120+
expect(result).toContain('[data-sentry-label="DeepForm"]');
121+
});
122+
123+
it('does not add prefix if data-sentry-label is already in cssSelector path', () => {
124+
const el = document.createElement('div');
125+
el.innerHTML = `<div data-sentry-label="OuterLabel">
126+
<div data-sentry-label="InnerLabel">
127+
<button id="btn" />
128+
</div>
129+
</div>`;
130+
document.body.appendChild(el);
131+
132+
expect(htmlTreeAsString(document.getElementById('btn'))).toBe('[data-sentry-label="InnerLabel"] > button#btn');
133+
});
134+
135+
it('returns normal cssSelector when no data-sentry-label exists', () => {
136+
const el = document.createElement('div');
137+
el.innerHTML = `<div class="container">
138+
<button id="no-label-btn" class="btn" />
139+
</div>`;
140+
document.body.appendChild(el);
141+
142+
expect(htmlTreeAsString(document.getElementById('no-label-btn'))).toBe(
143+
'body > div > div.container > button#no-label-btn.btn',
144+
);
145+
});
146+
147+
it('prioritizes data-sentry-label over data-sentry-component', () => {
148+
const el = document.createElement('div');
149+
el.innerHTML = '<button data-sentry-component="MyComponent" data-sentry-label="MyLabel" class="btn" />';
150+
document.body.appendChild(el);
151+
152+
expect(htmlTreeAsString(document.querySelector('button'))).toBe('body > div > [data-sentry-label="MyLabel"]');
153+
});
154+
});
76155
});

0 commit comments

Comments
 (0)