diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 70a6595d07d9..a8038ae17869 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -7,6 +7,7 @@ export { reportingObserverIntegration } from './integrations/reportingobserver'; export { httpClientIntegration } from './integrations/httpclient'; export { contextLinesIntegration } from './integrations/contextlines'; export { graphqlClientIntegration } from './integrations/graphqlClient'; +export { viewHierarchyIntegration } from './integrations/view-hierarchy'; export { captureConsoleIntegration, diff --git a/packages/browser/src/integrations/view-hierarchy.ts b/packages/browser/src/integrations/view-hierarchy.ts new file mode 100644 index 000000000000..3ca8a0122fa5 --- /dev/null +++ b/packages/browser/src/integrations/view-hierarchy.ts @@ -0,0 +1,125 @@ +import type { Attachment, Event, ViewHierarchyData, ViewHierarchyWindow } from '@sentry/core'; +import { defineIntegration, getComponentName } from '@sentry/core'; +import { WINDOW } from '../helpers'; + +interface OnElementArgs { + /** + * The element being processed. + */ + element: HTMLElement; + /** + * Lowercase tag name of the element. + */ + tagName: string; + /** + * The component name of the element. + */ + componentName?: string; +} + +interface Options { + /** + * Whether to attach the view hierarchy to the event. + * + * Default: Always attach. + */ + shouldAttach?: (event: Event) => boolean; + + /** + * Called for each HTMLElement as we walk the DOM. + * + * Return an object to include the element with any additional properties. + * Return `skip` to exclude the element and its children. + * Return `children` to skip the element but include its children. + */ + onElement?: (prop: OnElementArgs) => Record | 'skip' | 'children'; +} + +/** + * An integration to include a view hierarchy attachment which contains the DOM. + */ +export const viewHierarchyIntegration = defineIntegration((options: Options = {}) => { + const skipHtmlTags = ['script']; + + /** Walk an element */ + function walk(element: HTMLElement, windows: ViewHierarchyWindow[]): void { + // With Web Components, we need walk into shadow DOMs + const children = 'shadowRoot' in element && element.shadowRoot ? element.shadowRoot.children : element.children; + + for (const child of children) { + if (!(child instanceof HTMLElement)) { + continue; + } + + const componentName = getComponentName(child) || undefined; + const tagName = child.tagName.toLowerCase(); + + if (skipHtmlTags.includes(tagName)) { + continue; + } + + const result = options.onElement?.({ element: child, componentName, tagName }) || {}; + + if (result === 'skip') { + continue; + } + + // Skip this element but include its children + if (result === 'children') { + walk(child, windows); + continue; + } + + const { x, y, width, height } = child.getBoundingClientRect(); + + const window: ViewHierarchyWindow = { + identifier: (child.id || undefined) as string, + type: componentName || tagName, + visible: true, + alpha: 1, + height, + width, + x, + y, + ...result, + }; + + const children: ViewHierarchyWindow[] = []; + window.children = children; + + // Recursively walk the children + walk(child, window.children); + + windows.push(window); + } + } + + return { + name: 'ViewHierarchy', + processEvent: (event, hint) => { + if (options.shouldAttach && options.shouldAttach(event) === false) { + return event; + } + + const root: ViewHierarchyData = { + rendering_system: 'DOM', + positioning: 'absolute', + windows: [], + }; + + walk(WINDOW.document.body, root.windows); + + const attachment: Attachment = { + filename: 'view-hierarchy.json', + attachmentType: 'event.view_hierarchy', + contentType: 'application/json', + data: JSON.stringify(root), + }; + + hint.attachments = hint.attachments || []; + hint.attachments.push(attachment); + + return event; + }, + }; +}); diff --git a/packages/core/src/types-hoist/view-hierarchy.ts b/packages/core/src/types-hoist/view-hierarchy.ts index a066bfbe42e6..453f8c7daca8 100644 --- a/packages/core/src/types-hoist/view-hierarchy.ts +++ b/packages/core/src/types-hoist/view-hierarchy.ts @@ -14,5 +14,6 @@ export type ViewHierarchyWindow = { export type ViewHierarchyData = { rendering_system: string; + positioning?: 'absolute' | 'relative'; windows: ViewHierarchyWindow[]; };