Skip to content

feat(plugin-autocapture-browser): add shadow to hierarchy - #1937

Open
daniel-graham-amplitude wants to merge 1 commit into
SR-4788-zoning-shadow-dom-supportfrom
EPD-794-add-shadow-to-hierarchy
Open

feat(plugin-autocapture-browser): add shadow to hierarchy#1937
daniel-graham-amplitude wants to merge 1 commit into
SR-4788-zoning-shadow-dom-supportfrom
EPD-794-add-shadow-to-hierarchy

Conversation

@daniel-graham-amplitude

@daniel-graham-amplitude daniel-graham-amplitude commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Mark shadow elements in Hierarchy Nodes.

// example `[Amplitude] Element Hierarchy` property
[{
    "tag": "button",
    "index": 0,
    "indexOfType": 0,
    "id": "shadow-button",
    "attrs": {
        "type": "button"
    }
}, {
    "tag": "open-host",
    "index": 0,
    "indexOfType": 0,
    "attrs": {
        "data-amp-internal-shadow": "true" // new property
    },
    "shadow": true // new property
}, {
    "tag": "div",
    "index": 3,
    "indexOfType": 0,
    "prevSib": "button",
    "id": "app"
}, {
    "tag": "body",
    "index": 2,
    "indexOfType": 0,
    "prevSib": "head"
}]

Checklist

  • Does your PR title have the correct title format?
  • Does your PR have a breaking change?: No

Note

Low Risk
Additive event-property metadata and test-only harness changes; no auth or breaking API changes.

Overview
Element hierarchy nodes for autocapture now flag elements that live inside a shadow tree: getElementProperties sets shadow: true and adds internal data-amp-internal-shadow on attrs when getRootNode() is a ShadowRoot. The HierarchyNode type gains an optional shadow field.

The shadow DOM manual test page adds a toggle to flip ?shadow=on without editing the URL, and Amplitude init uses import.meta.env.VITE_AMPLITUDE_API_KEY instead of a hardcoded key.

Reviewed by Cursor Bugbot for commit ae97c17. Bugbot is set up for automated code reviews on this repo. Configure here.

@daniel-graham-amplitude
daniel-graham-amplitude requested a review from a team as a code owner August 13, 2026 21:46
@linear-code

linear-code Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

EPD-794

@daniel-graham-amplitude
daniel-graham-amplitude force-pushed the EPD-794-add-shadow-to-hierarchy branch from 05b0832 to 87f1a0d Compare August 13, 2026 21:48
Comment thread tools/shadow-harness/harness.mjs Outdated
Comment on lines +473 to +478
row.innerHTML =
`<span style="color:${color};font-weight:700">${kind}</span> ` +
`<span style="color:${ok ? '#7ee787' : '#ff7b72'}">${ok ? '✓ round-trips' : '✗ no round-trip'}</span>` +
(extra ? ` <span style="color:#8b949e">${extra}</span>` : '') +
`<span data-badge style="float:right;color:#8b949e"></span>` +
`<div style="margin-top:2px;color:#eee">${selector.replace(/</g, '&lt;')}</div>`;

@semgrep-amplitude semgrep-amplitude Bot Aug 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User controlled data in methods like innerHTML, outerHTML or document.write is an anti-pattern that can lead to XSS vulnerabilities

🧁 Removed in commit 87f1a0d 🧁

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Stale shadow tests use old APIs
    • Removed leftover shadow-capture.test.ts, which called helpers with pre-ShadowMode signatures and imported symbols helpers no longer export; the same cases are already covered against the current APIs.

Create PR

Or push these changes by commenting:

@cursor push 608ed1b003
Preview (608ed1b003)
diff --git a/packages/plugin-autocapture-browser/test/shadow-capture.test.ts b/packages/plugin-autocapture-browser/test/shadow-capture.test.ts
deleted file mode 100644
--- a/packages/plugin-autocapture-browser/test/shadow-capture.test.ts
+++ /dev/null
@@ -1,395 +1,0 @@
-/**
- * Shadow-DOM capture-side behavior: ancestor walks, deep discovery helpers, the
- * composedPath event-target resolution, and the MutationObserver fan-out.
- *
- * jsdom supports `attachShadow({ mode: 'open' })`, `getRootNode`,
- * `composedPath`, and `MutationObserver`, so these exercise the real paths.
- */
-/* eslint-disable @typescript-eslint/unbound-method, @typescript-eslint/no-unsafe-return */
-import {
-  getClosestElement,
-  collectOpenShadowRoots,
-  querySelectorAllDeep,
-  MAX_SHADOW_TRAVERSAL_NODES,
-} from '../src/helpers';
-import { getAncestors } from '../src/hierarchy';
-import { createMutationObservable } from '../src/observables';
-import { DataExtractor } from '../src/data-extractor';
-
-function attachOpen(host: Element, html: string): ShadowRoot {
-  const root = host.attachShadow({ mode: 'open' });
-  root.innerHTML = html;
-  return root;
-}
-
-/** Let jsdom's MutationObserver microtask flush. */
-const tick = () => new Promise((resolve) => setTimeout(resolve, 0));
-
-afterEach(() => {
-  document.body.innerHTML = '';
-});
-
-describe('getClosestElement — shadow crossing', () => {
-  let host: Element;
-  let inner: Element;
-
-  beforeEach(() => {
-    document.body.innerHTML = `<div id="app"><my-host></my-host></div>`;
-    host = document.querySelector('my-host') as Element;
-    const root = attachOpen(host, `<button id="cta">x</button>`);
-    inner = root.getElementById('cta') as Element;
-  });
-
-  it('does NOT cross the shadow boundary by default (returns null)', () => {
-    // The matching ancestor (#app) lives in the light DOM, outside the inner
-    // element's shadow tree. Default behavior stops at the tree boundary.
-    expect(getClosestElement(inner, ['#app'])).toBeNull();
-  });
-
-  it('crosses into the host and up to a light-DOM ancestor when enabled', () => {
-    expect(getClosestElement(inner, ['#app'], true, 1)).toBe(document.getElementById('app'));
-  });
-
-  it('respects the crossing budget (depth 0 cannot cross)', () => {
-    expect(getClosestElement(inner, ['#app'], true, 0)).toBeNull();
-  });
-
-  it('still matches within the same tree without crossing', () => {
-    expect(getClosestElement(inner, ['#cta'], false, 0)).toBe(inner);
-  });
-});
-
-describe('getAncestors — shadow crossing', () => {
-  let inner: Element;
-  let host: Element;
-
-  beforeEach(() => {
-    document.body.innerHTML = `<div id="app"><my-host></my-host></div>`;
-    host = document.querySelector('my-host') as Element;
-    const root = attachOpen(host, `<span id="inner">x</span>`);
-    inner = root.getElementById('inner') as Element;
-  });
-
-  it('stops at the shadow boundary by default (only the element itself)', () => {
-    // inner has no parentElement at the top of its shadow tree.
-    expect(getAncestors(inner)).toEqual([inner]);
-  });
-
-  it('walks through the host into the light DOM when enabled', () => {
-    const ancestors = getAncestors(inner, true, 1);
-    expect(ancestors).toContain(inner);
-    expect(ancestors).toContain(host);
-    expect(ancestors).toContain(document.getElementById('app'));
-  });
-});
-
-describe('collectOpenShadowRoots', () => {
-  it('returns nothing when depth budget is 0', () => {
-    document.body.innerHTML = `<my-host></my-host>`;
-    attachOpen(document.querySelector('my-host') as Element, `<div></div>`);
-    expect(collectOpenShadowRoots(document.body, 0)).toEqual([]);
-  });
-
-  it('finds nested open roots with their crossing depth, bounded by maxDepth', () => {
-    document.body.innerHTML = `<my-card></my-card>`;
-    const card = document.querySelector('my-card') as Element;
-    const cardRoot = attachOpen(card, `<my-button></my-button>`);
-    const button = cardRoot.querySelector('my-button') as Element;
-    attachOpen(button, `<button>x</button>`);
-
-    const depth1 = collectOpenShadowRoots(document.body, 1);
-    expect(depth1).toHaveLength(1);
-    expect(depth1[0].depth).toBe(1);
-    expect(depth1[0].root).toBe(cardRoot);
-
-    const depth2 = collectOpenShadowRoots(document.body, 2);
-    expect(depth2.map((r) => r.depth).sort()).toEqual([1, 2]);
-  });
-
-  it('skips closed shadow roots (invisible from outside)', () => {
-    document.body.innerHTML = `<my-host></my-host>`;
-    (document.querySelector('my-host') as Element).attachShadow({ mode: 'closed' });
-    expect(collectOpenShadowRoots(document.body, 5)).toEqual([]);
-  });
-});
-
-describe('querySelectorAllDeep', () => {
-  beforeEach(() => {
-    document.body.innerHTML = `<button class="track">light</button><my-host></my-host>`;
-    attachOpen(document.querySelector('my-host') as Element, `<button class="track">shadow</button>`);
-  });
-
-  it('returns only light-DOM matches when depth is 0', () => {
-    const found = querySelectorAllDeep(document, '.track', 0);
-    expect(found).toHaveLength(1);
-    expect(found[0].textContent).toBe('light');
-  });
-
-  it('pierces open shadow roots when depth allows', () => {
-    const found = querySelectorAllDeep(document, '.track', 1);
-    expect(found.map((el) => el.textContent).sort()).toEqual(['light', 'shadow']);
-  });
-});
-
-/**
- * Hardening / safety: none of the shadow capture-side helpers may throw into a
- * host page, even given a customer-supplied malformed selector, an exotic
- * (non-Element) event target, or a pathologically large DOM.
- */
-describe('shadow helpers — never throw on hostile input', () => {
-  it('querySelectorAllDeep returns light-DOM matches even if a shadow root query is malformed', () => {
-    // Valid selector against a normal tree still resolves; the guard only trips
-    // per-root, so a valid selector keeps working.
-    document.body.innerHTML = `<button class="track">x</button>`;
-    expect(querySelectorAllDeep(document, '.track', 1).map((el) => el.textContent)).toEqual(['x']);
-  });
-
-  it('getClosestElement does not throw when the start node lacks getRootNode (e.g. window)', () => {
-    const exotic = window as unknown as Element;
-    expect(() => getClosestElement(exotic, ['button'], true, 2)).not.toThrow();
-    expect(getClosestElement(exotic, ['button'], true, 2)).toBeNull();
-  });
-
-  it('querySelectorAllDeep returns [] for an empty selector string', () => {
-    document.body.innerHTML = `<button class="track">x</button>`;
-    expect(querySelectorAllDeep(document, '', 1)).toEqual([]);
-  });
-
-  it('querySelectorAllDeep defaults to light-DOM only (no depth arg) and accepts an Element root', () => {
-    document.body.innerHTML = `<div id="scope"><button class="track">x</button><my-host></my-host></div>`;
-    const scope = document.getElementById('scope') as Element;
-    attachOpen(scope.querySelector('my-host') as Element, `<button class="track">y</button>`);
-
-    // Default depth (arg omitted) → light DOM only.
-    expect(querySelectorAllDeep(scope, '.track').map((el) => el.textContent)).toEqual(['x']);
-    // Element (non-Document) root with depth → pierces the shadow root within scope.
-    expect(
-      querySelectorAllDeep(scope, '.track', 1)
-        .map((el) => el.textContent)
-        .sort(),
-    ).toEqual(['x', 'y']);
-  });
-
-  it('collectOpenShadowRoots is bounded by MAX_SHADOW_TRAVERSAL_NODES', () => {
-    expect(MAX_SHADOW_TRAVERSAL_NODES).toBeGreaterThan(0);
-    // A wide-but-shallow tree: the traversal must terminate and never throw.
-    const wide = document.createElement('div');
-    for (let i = 0; i < 100; i++) {
-      wide.appendChild(document.createElement('span'));
-    }
-    document.body.appendChild(wide);
-    expect(() => collectOpenShadowRoots(document.body, 1)).not.toThrow();
-  });
-});
-
-describe('DataExtractor.resolveEventTarget — composedPath', () => {
-  let extractor: DataExtractor;
-
-  beforeEach(() => {
-    extractor = new DataExtractor({});
-  });
-
-  afterEach(() => {
-    // Reset the shared engine config back to defaults so toggles don't leak.
-    extractor.updateSelectorConfig();
-  });
-
-  it('returns event.target (retargeted host) when shadow support is off', () => {
-    const host = document.createElement('my-host');
-    const inner = document.createElement('button');
-    const event = { target: host, composedPath: () => [inner, host] } as unknown as Event;
-    expect(extractor.resolveEventTarget(event)).toBe(host);
-  });
-
-  it('returns the true inner element from composedPath when shadow support is on', () => {
-    extractor.updateSelectorConfig({ shadowDomEnabled: true });
-    const host = document.createElement('my-host');
-    const inner = document.createElement('button');
-    const event = { target: host, composedPath: () => [inner, host] } as unknown as Event;
-    expect(extractor.resolveEventTarget(event)).toBe(inner);
-  });
-
-  it('falls back to event.target when composedPath is unavailable (shadow on)', () => {
-    extractor.updateSelectorConfig({ shadowDomEnabled: true });
-    const host = document.createElement('my-host');
-    // No composedPath on the event (e.g. an environment/event that lacks it).
-    const event = { target: host } as unknown as Event;
-    expect(extractor.resolveEventTarget(event)).toBe(host);
-  });
-});
-
-describe('createMutationObservable — shadow fan-out', () => {
-  it('does NOT observe shadow-root mutations when disabled', async () => {
-    document.body.innerHTML = `<my-host></my-host>`;
-    const root = attachOpen(document.querySelector('my-host') as Element, `<div id="sr"></div>`);
-
-    const batches: MutationRecord[][] = [];
-    const sub = createMutationObservable(() => ({ enabled: false, maxDepth: 1 })).subscribe((m) => batches.push(m));
-
-    // Mutate inside the shadow root — invisible to a body-only observer.
-    root.getElementById('sr')?.appendChild(document.createElement('span'));
-    await tick();
-
-    const sawShadowMutation = batches.some((b) => b.some((rec) => rec.target.getRootNode() === root));
-    expect(sawShadowMutation).toBe(false);
-    sub.unsubscribe();
-  });
-
-  it('observes mutations inside an existing open shadow root when enabled', async () => {
-    document.body.innerHTML = `<my-host></my-host>`;
-    const root = attachOpen(document.querySelector('my-host') as Element, `<div id="sr"></div>`);
-
-    const batches: MutationRecord[][] = [];
-    const sub = createMutationObservable(() => ({ enabled: true, maxDepth: 1 })).subscribe((m) => batches.push(m));
-
-    root.getElementById('sr')?.appendChild(document.createElement('span'));
-    await tick();
-
-    const sawShadowMutation = batches.some((b) => b.some((rec) => rec.target.getRootNode() === root));
-    expect(sawShadowMutation).toBe(true);
-    sub.unsubscribe();
-  });
-
-  it('attaches to a shadow root that mounts AFTER setup (late-loading component)', async () => {
-    document.body.innerHTML = '';
-    const batches: MutationRecord[][] = [];
-    const sub = createMutationObservable(() => ({ enabled: true, maxDepth: 1 })).subscribe((m) => batches.push(m));
-
-    // Mount a brand-new shadow host after the observable is already running.
-    const host = document.createElement('my-host');
-    document.body.appendChild(host);
-    const root = attachOpen(host, `<div id="late"></div>`);
-    await tick(); // body observer sees the host → attaches to its shadow root
-
-    // Now mutate inside the newly-attached shadow root.
-    root.getElementById('late')?.appendChild(document.createElement('span'));
-    await tick();
-
-    const sawShadowMutation = batches.some((b) => b.some((rec) => rec.target.getRootNode() === root));
-    expect(sawShadowMutation).toBe(true);
-    sub.unsubscribe();
-  });
-
-  it('observes mutations in a NESTED shadow root (depth 2) when the budget allows', async () => {
-    document.body.innerHTML = `<my-card></my-card>`;
-    const cardRoot = attachOpen(document.querySelector('my-card') as Element, `<my-button></my-button>`);
-    const innerRoot = attachOpen(cardRoot.querySelector('my-button') as Element, `<div id="deep"></div>`);
-
-    const batches: MutationRecord[][] = [];
-    const sub = createMutationObservable(() => ({ enabled: true, maxDepth: 2 })).subscribe((m) => batches.push(m));
-
-    innerRoot.getElementById('deep')?.appendChild(document.createElement('span'));
-    await tick();
-
-    const sawDeepMutation = batches.some((b) => b.some((rec) => rec.target.getRootNode() === innerRoot));
-    expect(sawDeepMutation).toBe(true);
-    sub.unsubscribe();
-  });
-
-  it('does NOT reach a nested (depth 2) shadow root when the budget is 1', async () => {
-    document.body.innerHTML = `<my-card></my-card>`;
-    const cardRoot = attachOpen(document.querySelector('my-card') as Element, `<my-button></my-button>`);
-    const innerRoot = attachOpen(cardRoot.querySelector('my-button') as Element, `<div id="deep"></div>`);
-
-    const batches: MutationRecord[][] = [];
-    const sub = createMutationObservable(() => ({ enabled: true, maxDepth: 1 })).subscribe((m) => batches.push(m));
-
-    innerRoot.getElementById('deep')?.appendChild(document.createElement('span'));
-    await tick();
-
-    const sawDeepMutation = batches.some((b) => b.some((rec) => rec.target.getRootNode() === innerRoot));
-    expect(sawDeepMutation).toBe(false);
-    sub.unsubscribe();
-  });
-
-  it('falls back to depth 0 when an observed shadow root is missing from the depth map', async () => {
-    document.body.innerHTML = `<outer-host></outer-host>`;
-    const outer = document.querySelector('outer-host') as Element;
-    const outerRoot = attachOpen(outer, `<div id="slot"></div>`);
-
-    const batches: MutationRecord[][] = [];
-    const sub = createMutationObservable(() => ({ enabled: true, maxDepth: 1 })).subscribe((m) => batches.push(m));
-
-    const originalGet = WeakMap.prototype.get;
-    const spy = jest
-      .spyOn(WeakMap.prototype, 'get')
-      .mockImplementation(function (this: WeakMap<object, unknown>, key: object) {
-        if (key === outerRoot) {
-          return undefined;
-        }
-        return originalGet.call(this, key);
-      });
-
-    try {
-      // Attach shadow before appending so the MO callback (which runs after this
-      // synchronous turn) discovers the nested root via collectOpenShadowRoots.
-      const nestedHost = document.createElement('nested-host');
-      const nestedRoot = attachOpen(nestedHost, `<div id="inner"></div>`);
-      outerRoot.getElementById('slot')?.appendChild(nestedHost);
-      await tick();
-
-      nestedRoot.getElementById('inner')?.appendChild(document.createElement('span'));
-      await tick();
-
-      const sawNested = batches.some((b) => b.some((rec) => rec.target.getRootNode() === nestedRoot));
-      expect(sawNested).toBe(true);
-    } finally {
-      spy.mockRestore();
-      sub.unsubscribe();
-    }
-  });
-
-  it('with no shadow-config getter, behaves like a plain body observer (off path)', async () => {
-    document.body.innerHTML = `<div id="light"></div>`;
-    const batches: MutationRecord[][] = [];
-    // No getter passed at all — the optional gate must default to disabled.
-    const sub = createMutationObservable().subscribe((m) => batches.push(m));
-
-    document.getElementById('light')?.appendChild(document.createElement('span'));
-    await tick();
-
-    expect(batches.length).toBeGreaterThan(0);
-    sub.unsubscribe();
-  });
-
-  it('does not double-observe a shadow root already attached (re-scanned host)', async () => {
-    document.body.innerHTML = `<my-host></my-host>`;
-    const host = document.querySelector('my-host') as Element;
-    const root = attachOpen(host, `<div id="sr"></div>`);
-
-    const batches: MutationRecord[][] = [];
-    const sub = createMutationObservable(() => ({ enabled: true, maxDepth: 1 })).subscribe((m) => batches.push(m));
-
-    // Re-append the existing host: generates a childList mutation whose addedNode
-    // is a host whose shadow root is ALREADY observed → observeRoot short-circuits.
-    document.body.appendChild(host);
-    await tick();
-
-    // Mutating inside the (still singly-observed) shadow root emits exactly once.
-    root.getElementById('sr')?.appendChild(document.createElement('span'));
-    await tick();
-
-    const shadowBatches = batches.filter((b) => b.some((rec) => rec.target.getRootNode() === root));
-    // The span insertion is reported once, not duplicated by a second observer.
-    const spanInsertions = shadowBatches
-      .flat()
-      .filter((rec) => Array.from(rec.addedNodes).some((n) => n.nodeName === 'SPAN'));
-    expect(spanInsertions).toHaveLength(1);
-    sub.unsubscribe();
-  });
-
-  it('still emits light-DOM mutations even if the shadow gate getter throws', async () => {
-    document.body.innerHTML = `<div id="light"></div>`;
-    const batches: MutationRecord[][] = [];
-    // A hostile getter that throws must not break event flow (observer.next runs first).
-    const sub = createMutationObservable(() => {
-      throw new Error('boom');
-    }).subscribe((m) => batches.push(m));
-
-    document.getElementById('light')?.appendChild(document.createElement('span'));
-    await tick();
-
-    expect(batches.length).toBeGreaterThan(0);
-    sub.unsubscribe();
-  });
-});
\ No newline at end of file

You can send follow-ups to the cloud agent here.

Comment thread packages/plugin-autocapture-browser/test/shadow-capture.test.ts Outdated
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
packages/analytics-browser/lib/scripts/amplitude-min.js.gz 63.56 KB (+0.19% 🔺)
packages/session-replay-browser/lib/scripts/session-replay-browser-min.js.gz 134.97 KB (+0.01% 🔺)
packages/unified/lib/scripts/amplitude-min.umd.js.gz 217.69 KB (+0.06% 🔺)
@amplitude/element-selector (gzipped esm) 3.4 KB (0%)

@daniel-graham-amplitude
daniel-graham-amplitude force-pushed the EPD-794-add-shadow-to-hierarchy branch from 87f1a0d to ed17e3e Compare August 13, 2026 21:55
@daniel-graham-amplitude daniel-graham-amplitude changed the title Epd 794 add shadow to hierarchy feat(plugin-autocapture-browser): add shadow to hierarchy Aug 17, 2026
@daniel-graham-amplitude
daniel-graham-amplitude force-pushed the EPD-794-add-shadow-to-hierarchy branch from ed17e3e to ae97c17 Compare August 18, 2026 17:05

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Wrong shadow host detection
    • getElementProperties now marks shadow hosts via element.shadowRoot, and the unit test uses attachShadow so it exercises the host rather than in-shadow nodes.

Create PR

Or push these changes by commenting:

@cursor push bd93818677
Preview (bd93818677)
diff --git a/packages/plugin-autocapture-browser/src/hierarchy.ts b/packages/plugin-autocapture-browser/src/hierarchy.ts
--- a/packages/plugin-autocapture-browser/src/hierarchy.ts
+++ b/packages/plugin-autocapture-browser/src/hierarchy.ts
@@ -117,7 +117,7 @@
     properties.attrs = attributes;
   }
 
-  if (isShadowRoot(element.getRootNode())) {
+  if (element.shadowRoot) {
     properties.attrs = { ...(properties.attrs || {}), 'data-amp-internal-shadow': 'true' };
     properties.shadow = true;
   }

diff --git a/packages/plugin-autocapture-browser/test/hierarchy.test.ts b/packages/plugin-autocapture-browser/test/hierarchy.test.ts
--- a/packages/plugin-autocapture-browser/test/hierarchy.test.ts
+++ b/packages/plugin-autocapture-browser/test/hierarchy.test.ts
@@ -92,7 +92,7 @@
       });
     });
 
-    test('should capture shadow attributes when element is in a shadow root', () => {
+    test('should capture shadow attributes when element is a shadow host', () => {
       document.getElementsByTagName('body')[0].innerHTML = `
         <div id="container">
           <div id="inner">
@@ -100,8 +100,8 @@
           </div>
         </div>
       `;
-      const inner = document.getElementById('inner') as any;
-      Object.defineProperty(inner, 'shadowRoot', { value: document.createElement('div') });
+      const inner = document.getElementById('inner') as HTMLElement;
+      inner.attachShadow({ mode: 'open' });
       expect(HierarchyUtil.getElementProperties(inner, new Set())).toEqual({
         id: 'inner',
         index: 0,

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ae97c17. Configure here.

if (isShadowRoot(element.getRootNode())) {
properties.attrs = { ...(properties.attrs || {}), 'data-amp-internal-shadow': 'true' };
properties.shadow = true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong shadow host detection

High Severity

getElementProperties uses isShadowRoot(element.getRootNode()), which marks nodes inside a shadow tree. The PR example and new test expect the shadow host (for example open-host) to get shadow: true and data-amp-internal-shadow. Hosts live in the light DOM, so getRootNode() is the document and they are never marked; in-shadow targets are marked instead. The unit test also stubs shadowRoot on a light-DOM node rather than building a real shadow tree, so it does not exercise the intended case and fails against this check.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ae97c17. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant