From 1c0b9cead6d77875a25e98982f66931bc65d6e29 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sat, 11 Jan 2025 19:26:42 +0100 Subject: [PATCH 01/12] feat(browser): Add browser View Hierarchy integration --- packages/browser/src/index.ts | 1 + .../src/integrations/view-hierarchy.ts | 115 ++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 packages/browser/src/integrations/view-hierarchy.ts diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 56c7dd449602..8d2cef356e3c 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -69,3 +69,4 @@ export { } from './integrations/featureFlags'; export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly'; export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature'; +export { viewHierarchyIntegration } from './integrations/view-hierarchy'; diff --git a/packages/browser/src/integrations/view-hierarchy.ts b/packages/browser/src/integrations/view-hierarchy.ts new file mode 100644 index 000000000000..36ad716fdb87 --- /dev/null +++ b/packages/browser/src/integrations/view-hierarchy.ts @@ -0,0 +1,115 @@ +import type { Attachment, Event, ViewHierarchyData, ViewHierarchyWindow } from '@sentry/core'; +import { defineIntegration, dropUndefinedKeys, 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. + */ + 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: { children: HTMLCollection }, windows: ViewHierarchyWindow[]): void { + for (const child of element.children) { + if (!(child instanceof HTMLElement)) { + continue; + } + + const componentName = getComponentName(child) || undefined; + const tagName = child.tagName.toLowerCase(); + const result = options.onElement?.({ element: child, componentName, tagName }) || {}; + + // Skip this element and its children + if (skipHtmlTags.includes(tagName) || result === 'skip') { + continue; + } + + // Skip this element but include its children + if (result === 'children') { + walk('shadowRoot' in child && child.shadowRoot ? child.shadowRoot : child, windows); + continue; + } + + const childRect = child.getBoundingClientRect(); + + const window: ViewHierarchyWindow = dropUndefinedKeys({ + identifier: (child.id || undefined) as string, + type: componentName || tagName, + visible: true, + alpha: 1, + height: childRect.height, + width: childRect.width, + x: childRect.x, + y: childRect.y, + ...result, + }); + + const children: ViewHierarchyWindow[] = []; + window.children = children; + + // Recursively walk the children + walk('shadowRoot' in child && child.shadowRoot ? child.shadowRoot : 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', + 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; + }, + }; +}); From 16d91537dd06425333bd88a41bbf3f694704b519 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 11 Feb 2026 16:05:01 +0100 Subject: [PATCH 02/12] Changes after rcf merge --- packages/browser/src/index.ts | 1 + .../src/integrations/view-hierarchy.ts | 38 ++++++++++++------- .../core/src/types-hoist/view-hierarchy.ts | 1 + 3 files changed, 26 insertions(+), 14 deletions(-) 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 index 36ad716fdb87..3ca8a0122fa5 100644 --- a/packages/browser/src/integrations/view-hierarchy.ts +++ b/packages/browser/src/integrations/view-hierarchy.ts @@ -1,5 +1,5 @@ import type { Attachment, Event, ViewHierarchyData, ViewHierarchyWindow } from '@sentry/core'; -import { defineIntegration, dropUndefinedKeys, getComponentName } from '@sentry/core'; +import { defineIntegration, getComponentName } from '@sentry/core'; import { WINDOW } from '../helpers'; interface OnElementArgs { @@ -20,6 +20,8 @@ interface OnElementArgs { interface Options { /** * Whether to attach the view hierarchy to the event. + * + * Default: Always attach. */ shouldAttach?: (event: Event) => boolean; @@ -40,46 +42,53 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} const skipHtmlTags = ['script']; /** Walk an element */ - function walk(element: { children: HTMLCollection }, windows: ViewHierarchyWindow[]): void { - for (const child of element.children) { + 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 }) || {}; - // Skip this element and its children - if (skipHtmlTags.includes(tagName) || result === 'skip') { + if (result === 'skip') { continue; } // Skip this element but include its children if (result === 'children') { - walk('shadowRoot' in child && child.shadowRoot ? child.shadowRoot : child, windows); + walk(child, windows); continue; } - const childRect = child.getBoundingClientRect(); + const { x, y, width, height } = child.getBoundingClientRect(); - const window: ViewHierarchyWindow = dropUndefinedKeys({ + const window: ViewHierarchyWindow = { identifier: (child.id || undefined) as string, type: componentName || tagName, visible: true, alpha: 1, - height: childRect.height, - width: childRect.width, - x: childRect.x, - y: childRect.y, + height, + width, + x, + y, ...result, - }); + }; const children: ViewHierarchyWindow[] = []; window.children = children; // Recursively walk the children - walk('shadowRoot' in child && child.shadowRoot ? child.shadowRoot : child, window.children); + walk(child, window.children); windows.push(window); } @@ -94,6 +103,7 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} const root: ViewHierarchyData = { rendering_system: 'DOM', + positioning: 'absolute', windows: [], }; 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[]; }; From 899e5c067713f39edb4f6b6a81bee488b7a8c3b2 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 26 Feb 2026 23:09:37 +0000 Subject: [PATCH 03/12] more --- .../browser/src/integrations/view-hierarchy.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/browser/src/integrations/view-hierarchy.ts b/packages/browser/src/integrations/view-hierarchy.ts index 3ca8a0122fa5..aa3bc5b8f835 100644 --- a/packages/browser/src/integrations/view-hierarchy.ts +++ b/packages/browser/src/integrations/view-hierarchy.ts @@ -1,4 +1,4 @@ -import type { Attachment, Event, ViewHierarchyData, ViewHierarchyWindow } from '@sentry/core'; +import type { Attachment, Event, EventHint, ViewHierarchyData, ViewHierarchyWindow } from '@sentry/core'; import { defineIntegration, getComponentName } from '@sentry/core'; import { WINDOW } from '../helpers'; @@ -23,7 +23,14 @@ interface Options { * * Default: Always attach. */ - shouldAttach?: (event: Event) => boolean; + shouldAttach?: (event: Event, hint: EventHint) => boolean; + + /** + * A function that returns the root element to start walking the DOM from. + * + * Default: `window.document.body` + */ + rootElement?: () => HTMLElement | undefined; /** * Called for each HTMLElement as we walk the DOM. @@ -43,7 +50,7 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} /** Walk an element */ function walk(element: HTMLElement, windows: ViewHierarchyWindow[]): void { - // With Web Components, we need walk into shadow DOMs + // With Web Components, we need to walk into shadow DOMs const children = 'shadowRoot' in element && element.shadowRoot ? element.shadowRoot.children : element.children; for (const child of children) { @@ -97,7 +104,7 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} return { name: 'ViewHierarchy', processEvent: (event, hint) => { - if (options.shouldAttach && options.shouldAttach(event) === false) { + if (options.shouldAttach && options.shouldAttach(event, hint) === false) { return event; } @@ -107,7 +114,7 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} windows: [], }; - walk(WINDOW.document.body, root.windows); + walk(options.rootElement?.() || WINDOW.document.body, root.windows); const attachment: Attachment = { filename: 'view-hierarchy.json', From 50629fb43614696d43930979974134c36bf2ef71 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 5 Apr 2026 22:52:32 +0200 Subject: [PATCH 04/12] fix linting --- .../suites/tracing/worker-service-binding/index-sub-worker.ts | 2 +- packages/browser/src/integrations/view-hierarchy.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/worker-service-binding/index-sub-worker.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/worker-service-binding/index-sub-worker.ts index 06c79931b880..28737c16a448 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/worker-service-binding/index-sub-worker.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/worker-service-binding/index-sub-worker.ts @@ -5,7 +5,7 @@ interface Env { } const myWorker = { - async fetch(request: Request) { + async fetch(_: Request) { return new Response('Hello from another worker!'); }, }; diff --git a/packages/browser/src/integrations/view-hierarchy.ts b/packages/browser/src/integrations/view-hierarchy.ts index aa3bc5b8f835..6fe63949bf35 100644 --- a/packages/browser/src/integrations/view-hierarchy.ts +++ b/packages/browser/src/integrations/view-hierarchy.ts @@ -104,7 +104,7 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} return { name: 'ViewHierarchy', processEvent: (event, hint) => { - if (options.shouldAttach && options.shouldAttach(event, hint) === false) { + if (options.shouldAttach?.(event, hint) === false) { return event; } From 9aa2489b126c8b7bcbaaa6fd63b45f68365439dc Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 5 Apr 2026 23:20:30 +0200 Subject: [PATCH 05/12] Add integration test --- .../suites/integrations/viewHierarchy/init.js | 9 +++++ .../integrations/viewHierarchy/subject.js | 1 + .../integrations/viewHierarchy/template.html | 11 ++++++ .../suites/integrations/viewHierarchy/test.ts | 34 +++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js new file mode 100644 index 000000000000..16e92edb9230 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; +import { viewHierarchyIntegration } from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [viewHierarchyIntegration()], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/subject.js b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/subject.js new file mode 100644 index 000000000000..f7060a33f05c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/subject.js @@ -0,0 +1 @@ +throw new Error('Some error'); diff --git a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/template.html b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/template.html new file mode 100644 index 000000000000..9e600d2a7e60 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/template.html @@ -0,0 +1,11 @@ + + + + + + + +

Some title

+

Some text

+ + diff --git a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts new file mode 100644 index 000000000000..f83c9cbddaea --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts @@ -0,0 +1,34 @@ +import { expect } from '@playwright/test'; +import type { ViewHierarchyData } from '@sentry/core'; +import { sentryTest } from '../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, envelopeParser } from '../../../utils/helpers'; + +sentryTest('Captures view hierarchy as attachment', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const [, events] = await Promise.all([ + page.goto(url), + getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + req => envelopeParser(req)?.[4] as ViewHierarchyData, + ), + ]); + + expect(events).toHaveLength(1); + const event: ViewHierarchyData = events[0]; + + expect(event.rendering_system).toBe('DOM'); + expect(event.positioning).toBe('absolute'); + expect(event.windows).toHaveLength(2); + expect(event.windows[0].type).toBe('h1'); + expect(event.windows[0].visible).toBe(true); + expect(event.windows[0].alpha).toBe(1); + expect(event.windows[0].children).toHaveLength(0); + + expect(event.windows[1].type).toBe('p'); + expect(event.windows[1].visible).toBe(true); + expect(event.windows[1].alpha).toBe(1); + expect(event.windows[1].children).toHaveLength(0); +}); From e8a8d7b8eaf050d397b52b03d0a3e63d614b4daa Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 5 Apr 2026 23:35:53 +0200 Subject: [PATCH 06/12] Skip for cdn bundles --- .../suites/integrations/viewHierarchy/test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts index f83c9cbddaea..9cb715fe01c7 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts @@ -1,9 +1,13 @@ import { expect } from '@playwright/test'; import type { ViewHierarchyData } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, envelopeParser } from '../../../utils/helpers'; +import { getMultipleSentryEnvelopeRequests, envelopeParser, shouldSkipTracingTest } from '../../../utils/helpers'; sentryTest('Captures view hierarchy as attachment', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + return; + } + const url = await getLocalTestUrl({ testDir: __dirname }); const [, events] = await Promise.all([ From f225a3b3ed91f0c82ec906b1cfe4bafd52b07cab Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 5 Apr 2026 23:38:46 +0200 Subject: [PATCH 07/12] add null check from PR review --- packages/browser/src/integrations/view-hierarchy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/browser/src/integrations/view-hierarchy.ts b/packages/browser/src/integrations/view-hierarchy.ts index 6fe63949bf35..0241e696042b 100644 --- a/packages/browser/src/integrations/view-hierarchy.ts +++ b/packages/browser/src/integrations/view-hierarchy.ts @@ -50,6 +50,10 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} /** Walk an element */ function walk(element: HTMLElement, windows: ViewHierarchyWindow[]): void { + if (!element) { + return; + } + // With Web Components, we need to walk into shadow DOMs const children = 'shadowRoot' in element && element.shadowRoot ? element.shadowRoot.children : element.children; From 61243cda717fc482761bae46a3612e1708faa324 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sun, 5 Apr 2026 23:56:36 +0200 Subject: [PATCH 08/12] only run for non-cdn bundles --- .../suites/integrations/viewHierarchy/test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts index 9cb715fe01c7..4c399df879f0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts @@ -4,8 +4,9 @@ import { sentryTest } from '../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests, envelopeParser, shouldSkipTracingTest } from '../../../utils/helpers'; sentryTest('Captures view hierarchy as attachment', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - return; + const bundle = process.env.PW_BUNDLE; + if (bundle != null && !bundle.includes('esm') && !bundle.includes('cjs')) { + sentryTest.skip(); } const url = await getLocalTestUrl({ testDir: __dirname }); From 56e005c8b7b49b2f93dd1eb4c0e4565a8ec97ac2 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 6 Apr 2026 00:10:46 +0200 Subject: [PATCH 09/12] lint --- .../suites/integrations/viewHierarchy/test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts index 4c399df879f0..d3caf6ff9b3e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/viewHierarchy/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import type { ViewHierarchyData } from '@sentry/core'; import { sentryTest } from '../../../utils/fixtures'; -import { getMultipleSentryEnvelopeRequests, envelopeParser, shouldSkipTracingTest } from '../../../utils/helpers'; +import { getMultipleSentryEnvelopeRequests, envelopeParser } from '../../../utils/helpers'; sentryTest('Captures view hierarchy as attachment', async ({ getLocalTestUrl, page }) => { const bundle = process.env.PW_BUNDLE; From e5768ca6fb760b4088e0feb1fb5655313a470edc Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 6 Apr 2026 00:22:19 +0200 Subject: [PATCH 10/12] Limit `getComponentName` traverse depth --- packages/browser/src/integrations/view-hierarchy.ts | 2 +- packages/core/src/utils/browser.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/integrations/view-hierarchy.ts b/packages/browser/src/integrations/view-hierarchy.ts index 0241e696042b..ddba361b2e85 100644 --- a/packages/browser/src/integrations/view-hierarchy.ts +++ b/packages/browser/src/integrations/view-hierarchy.ts @@ -62,7 +62,7 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} continue; } - const componentName = getComponentName(child) || undefined; + const componentName = getComponentName(child, 1) || undefined; const tagName = child.tagName.toLowerCase(); if (skipHtmlTags.includes(tagName)) { diff --git a/packages/core/src/utils/browser.ts b/packages/core/src/utils/browser.ts index 6c062f8f6f60..9237af237ba2 100644 --- a/packages/core/src/utils/browser.ts +++ b/packages/core/src/utils/browser.ts @@ -145,15 +145,14 @@ export function getLocationHref(): string { * * @returns a string representation of the component for the provided DOM element, or `null` if not found */ -export function getComponentName(elem: unknown): string | null { +export function getComponentName(elem: unknown, maxTraverseHeight: number = 5): string | null { // @ts-expect-error WINDOW has HTMLElement if (!WINDOW.HTMLElement) { return null; } let currentElem = elem as SimpleNode; - const MAX_TRAVERSE_HEIGHT = 5; - for (let i = 0; i < MAX_TRAVERSE_HEIGHT; i++) { + for (let i = 0; i < maxTraverseHeight; i++) { if (!currentElem) { return null; } From a733a153121cab600c0cca80380df353fb50edcb Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 7 Apr 2026 13:08:06 +0200 Subject: [PATCH 11/12] Allow limiting traversal depth via `onElement` callback --- .../browser/src/integrations/view-hierarchy.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/browser/src/integrations/view-hierarchy.ts b/packages/browser/src/integrations/view-hierarchy.ts index ddba361b2e85..c62abba9a18c 100644 --- a/packages/browser/src/integrations/view-hierarchy.ts +++ b/packages/browser/src/integrations/view-hierarchy.ts @@ -15,6 +15,13 @@ interface OnElementArgs { * The component name of the element. */ componentName?: string; + + /** + * The current depth of the element in the view hierarchy. The root element will have a depth of 0. + * + * This allows you to limit the traversal depth for large DOM trees. + */ + depth?: number; } interface Options { @@ -49,7 +56,7 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} const skipHtmlTags = ['script']; /** Walk an element */ - function walk(element: HTMLElement, windows: ViewHierarchyWindow[]): void { + function walk(element: HTMLElement, windows: ViewHierarchyWindow[], depth = 0): void { if (!element) { return; } @@ -69,7 +76,7 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} continue; } - const result = options.onElement?.({ element: child, componentName, tagName }) || {}; + const result = options.onElement?.({ element: child, componentName, tagName, depth }) || {}; if (result === 'skip') { continue; @@ -77,7 +84,7 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} // Skip this element but include its children if (result === 'children') { - walk(child, windows); + walk(child, windows, depth + 1); continue; } @@ -99,7 +106,7 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} window.children = children; // Recursively walk the children - walk(child, window.children); + walk(child, window.children, depth + 1); windows.push(window); } From b0650401eb05493bc3d30a9dd7eb394fd82dd328 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 7 Apr 2026 13:23:54 +0200 Subject: [PATCH 12/12] only capture for events --- packages/browser/src/integrations/view-hierarchy.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/integrations/view-hierarchy.ts b/packages/browser/src/integrations/view-hierarchy.ts index c62abba9a18c..fa35ad7e00a2 100644 --- a/packages/browser/src/integrations/view-hierarchy.ts +++ b/packages/browser/src/integrations/view-hierarchy.ts @@ -115,7 +115,8 @@ export const viewHierarchyIntegration = defineIntegration((options: Options = {} return { name: 'ViewHierarchy', processEvent: (event, hint) => { - if (options.shouldAttach?.(event, hint) === false) { + // only capture for error events + if (event.type !== undefined || options.shouldAttach?.(event, hint) === false) { return event; }