Skip to content

Commit fe3b77c

Browse files
logaretmclaude
andcommitted
fix(replay): use live click attributes in breadcrumbs
Co-Authored-By: GPT-5 <noreply@anthropic.com>
1 parent 6b5e16f commit fe3b77c

2 files changed

Lines changed: 78 additions & 16 deletions

File tree

packages/replay-internal/src/coreHandlers/handleDom.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,30 @@ export function getBaseDomBreadcrumb(target: Node | null, message: string): Brea
5353
const node = nodeId && record.mirror.getNode(nodeId);
5454
const meta = node && record.mirror.getMeta(node);
5555
const element = meta && isElement(meta) ? meta : null;
56+
const liveElement = target instanceof Element && nodeId > -1 ? target : null;
5657

5758
return {
5859
message,
59-
data: element
60-
? {
61-
nodeId,
62-
node: {
63-
id: nodeId,
64-
tagName: element.tagName,
65-
textContent: Array.from(element.childNodes)
66-
.map((node: serializedNodeWithId) => node.type === NodeType.Text && node.textContent)
67-
.filter(Boolean) // filter out empty values
68-
.map(text => (text as string).trim())
69-
.join(''),
70-
attributes: getAttributesToRecord(element.attributes),
71-
},
72-
}
73-
: {},
60+
data:
61+
element || liveElement
62+
? {
63+
nodeId,
64+
node: {
65+
id: nodeId,
66+
tagName: element?.tagName || liveElement?.tagName.toLowerCase() || '',
67+
textContent: element
68+
? Array.from(element.childNodes)
69+
.map((node: serializedNodeWithId) => node.type === NodeType.Text && node.textContent)
70+
.filter(Boolean) // filter out empty values
71+
.map(text => (text as string).trim())
72+
.join('')
73+
: '',
74+
attributes: getAttributesToRecord(
75+
liveElement ? getElementAttributes(liveElement) : element?.attributes || {},
76+
),
77+
},
78+
}
79+
: {},
7480
};
7581
}
7682

@@ -107,3 +113,10 @@ function getDomTarget(handlerData: HandlerDataDom): { target: Node | null; messa
107113
function isElement(node: serializedNodeWithId): node is serializedElementNodeWithId {
108114
return node.type === NodeType.Element;
109115
}
116+
117+
function getElementAttributes(element: Element): Record<string, string> {
118+
return Array.from(element.attributes).reduce<Record<string, string>>((attributes, attribute) => {
119+
attributes[attribute.name] = attribute.value;
120+
return attributes;
121+
}, {});
122+
}

packages/replay-internal/test/unit/coreHandlers/handleDom.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
*/
44

55
import type { HandlerDataDom } from '@sentry/core';
6-
import { describe, expect, test } from 'vitest';
6+
import { record } from '@sentry-internal/rrweb';
7+
import { afterEach, describe, expect, test, vi } from 'vitest';
78
import { handleDom } from '../../../src/coreHandlers/handleDom';
89

910
describe('Unit | coreHandlers | handleDom', () => {
11+
afterEach(() => {
12+
vi.restoreAllMocks();
13+
});
14+
1015
test('it works with a basic click event on a div', () => {
1116
const parent = document.createElement('body');
1217
const target = document.createElement('div');
@@ -132,4 +137,48 @@ describe('Unit | coreHandlers | handleDom', () => {
132137
type: 'default',
133138
});
134139
});
140+
141+
test('it prefers live element attributes over stale rrweb mirror metadata', () => {
142+
const target = document.createElement('button');
143+
target.setAttribute('id', 'save-note-button');
144+
target.setAttribute('data-testid', 'save-note-button');
145+
target.textContent = 'Save Note';
146+
147+
vi.spyOn(record.mirror, 'getId').mockReturnValue(42);
148+
vi.spyOn(record.mirror, 'getNode').mockReturnValue(target);
149+
vi.spyOn(record.mirror, 'getMeta').mockReturnValue({
150+
id: 42,
151+
type: 2,
152+
tagName: 'button',
153+
childNodes: [{ id: 43, type: 3, textContent: 'Save Note' }],
154+
attributes: {
155+
id: 'next-question-button',
156+
'data-testid': 'next-question-button',
157+
},
158+
});
159+
160+
const actual = handleDom({
161+
name: 'click',
162+
event: { target },
163+
});
164+
165+
expect(actual).toEqual({
166+
category: 'ui.click',
167+
data: {
168+
nodeId: 42,
169+
node: {
170+
id: 42,
171+
tagName: 'button',
172+
textContent: 'Save Note',
173+
attributes: {
174+
id: 'save-note-button',
175+
testId: 'save-note-button',
176+
},
177+
},
178+
},
179+
message: 'button#save-note-button',
180+
timestamp: expect.any(Number),
181+
type: 'default',
182+
});
183+
});
135184
});

0 commit comments

Comments
 (0)