Skip to content

Commit 1c0b9ce

Browse files
committed
feat(browser): Add browser View Hierarchy integration
1 parent 3aa9078 commit 1c0b9ce

File tree

2 files changed

+116
-0
lines changed

2 files changed

+116
-0
lines changed

packages/browser/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,4 @@ export {
6969
} from './integrations/featureFlags';
7070
export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly';
7171
export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature';
72+
export { viewHierarchyIntegration } from './integrations/view-hierarchy';
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import type { Attachment, Event, ViewHierarchyData, ViewHierarchyWindow } from '@sentry/core';
2+
import { defineIntegration, dropUndefinedKeys, getComponentName } from '@sentry/core';
3+
import { WINDOW } from '../helpers';
4+
5+
interface OnElementArgs {
6+
/**
7+
* The element being processed.
8+
*/
9+
element: HTMLElement;
10+
/**
11+
* Lowercase tag name of the element.
12+
*/
13+
tagName: string;
14+
/**
15+
* The component name of the element.
16+
*/
17+
componentName?: string;
18+
}
19+
20+
interface Options {
21+
/**
22+
* Whether to attach the view hierarchy to the event.
23+
*/
24+
shouldAttach?: (event: Event) => boolean;
25+
26+
/**
27+
* Called for each HTMLElement as we walk the DOM.
28+
*
29+
* Return an object to include the element with any additional properties.
30+
* Return `skip` to exclude the element and its children.
31+
* Return `children` to skip the element but include its children.
32+
*/
33+
onElement?: (prop: OnElementArgs) => Record<string, string | number | boolean> | 'skip' | 'children';
34+
}
35+
36+
/**
37+
* An integration to include a view hierarchy attachment which contains the DOM.
38+
*/
39+
export const viewHierarchyIntegration = defineIntegration((options: Options = {}) => {
40+
const skipHtmlTags = ['script'];
41+
42+
/** Walk an element */
43+
function walk(element: { children: HTMLCollection }, windows: ViewHierarchyWindow[]): void {
44+
for (const child of element.children) {
45+
if (!(child instanceof HTMLElement)) {
46+
continue;
47+
}
48+
49+
const componentName = getComponentName(child) || undefined;
50+
const tagName = child.tagName.toLowerCase();
51+
const result = options.onElement?.({ element: child, componentName, tagName }) || {};
52+
53+
// Skip this element and its children
54+
if (skipHtmlTags.includes(tagName) || result === 'skip') {
55+
continue;
56+
}
57+
58+
// Skip this element but include its children
59+
if (result === 'children') {
60+
walk('shadowRoot' in child && child.shadowRoot ? child.shadowRoot : child, windows);
61+
continue;
62+
}
63+
64+
const childRect = child.getBoundingClientRect();
65+
66+
const window: ViewHierarchyWindow = dropUndefinedKeys({
67+
identifier: (child.id || undefined) as string,
68+
type: componentName || tagName,
69+
visible: true,
70+
alpha: 1,
71+
height: childRect.height,
72+
width: childRect.width,
73+
x: childRect.x,
74+
y: childRect.y,
75+
...result,
76+
});
77+
78+
const children: ViewHierarchyWindow[] = [];
79+
window.children = children;
80+
81+
// Recursively walk the children
82+
walk('shadowRoot' in child && child.shadowRoot ? child.shadowRoot : child, window.children);
83+
84+
windows.push(window);
85+
}
86+
}
87+
88+
return {
89+
name: 'ViewHierarchy',
90+
processEvent: (event, hint) => {
91+
if (options.shouldAttach && options.shouldAttach(event) === false) {
92+
return event;
93+
}
94+
95+
const root: ViewHierarchyData = {
96+
rendering_system: 'DOM',
97+
windows: [],
98+
};
99+
100+
walk(WINDOW.document.body, root.windows);
101+
102+
const attachment: Attachment = {
103+
filename: 'view-hierarchy.json',
104+
attachmentType: 'event.view_hierarchy',
105+
contentType: 'application/json',
106+
data: JSON.stringify(root),
107+
};
108+
109+
hint.attachments = hint.attachments || [];
110+
hint.attachments.push(attachment);
111+
112+
return event;
113+
},
114+
};
115+
});

0 commit comments

Comments
 (0)