Skip to content

feat(zoning): add support for shadow dom - #1922

Open
cely404 wants to merge 3 commits into
mainfrom
SR-4788-zoning-shadow-dom-support
Open

feat(zoning): add support for shadow dom#1922
cely404 wants to merge 3 commits into
mainfrom
SR-4788-zoning-shadow-dom-support

Conversation

@cely404

@cely404 cely404 commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds remotely gated Shadow DOM traversal and autocapture support across element selection, event enrichment, and observers.

Changes:

Adds depth-bounded Shadow DOM selector generation and resolution.
Extends autocapture, mutation, and exposure handling into open shadow roots.
Adds documentation, unit/E2E coverage, and manual performance guards.

Checklist

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

Note

Medium Risk
Touches core autocapture DOM traversal and selector generation on customer pages; mitigated by default-off flag, latch semantics, traversal caps, and differential tests proving the off path is unchanged.

Overview
Adds remotely gated Shadow DOM support (shadowDomEnabled, maxShadowDomDepth) that is separate from the v1 selector engine kill switch. With piercing on, @amplitude/element-selector emits composite selectors joined by >>> and consumers must re-resolve via resolveSelector; with it off, output stays byte-identical to pre-shadow behavior.

The autocapture plugin introduces a per-page ShadowGate that latches on the first config delivery that enables shadow support (disabling mid-session has no effect until reload). Capture uses composedPath / composed ancestor walks, deep querySelectorAll, and one MutationObserver per discovered open shadow root for mutations and exposure—plus a one-time scan when the gate arms.

Docs and tests: SHADOW-DOM.md, schema/README updates, broad unit coverage (kill-switch differential, round-trips), Playwright e2e for clicks, and a manual-only perf spec excluded from CI.

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

@linear-code

linear-code Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

SR-4788

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
packages/analytics-browser/lib/scripts/amplitude-min.js.gz 63.49 KB (+3.11% 🔺)
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.62 KB (+0.93% 🔺)
@amplitude/element-selector (gzipped esm) 3.4 KB (+27.56% 🔺)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds remotely gated Shadow DOM traversal and autocapture support across element selection, event enrichment, and observers.

Changes:

  • Adds depth-bounded Shadow DOM selector generation and resolution.
  • Extends autocapture, mutation, and exposure handling into open shadow roots.
  • Adds documentation, unit/E2E coverage, and manual performance guards.

Reviewed changes

Copilot reviewed 39 out of 39 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test-server/shadow-dom-test.html Adds Shadow DOM E2E fixture.
test-server/shadow-dom-perf.html Adds performance workload fixture.
playwright.config.ts Excludes manual performance tests from CI.
packages/plugin-autocapture-browser/test/shadow-gate.test.ts Tests gate and late-config behavior.
packages/plugin-autocapture-browser/test/observables.test.ts Tests shadow-aware observers.
packages/plugin-autocapture-browser/test/observables-coverage.test.ts Covers missing-body handling.
packages/plugin-autocapture-browser/test/hierarchy.test.ts Tests composed hierarchies.
packages/plugin-autocapture-browser/test/helpers.test.ts Tests shadow traversal helpers.
packages/plugin-autocapture-browser/test/default-event-tracking-advanced.test.ts Tests setup failure handling.
packages/plugin-autocapture-browser/test/data-extractor.test.ts Tests masking across shadow boundaries.
packages/plugin-autocapture-browser/src/shadow-mode.ts Implements the shared shadow gate.
packages/plugin-autocapture-browser/src/observables.ts Observes open shadow roots and exposures.
packages/plugin-autocapture-browser/src/hierarchy.ts Adds composed ancestor traversal.
packages/plugin-autocapture-browser/src/helpers.ts Adds deep queries and target resolution.
packages/plugin-autocapture-browser/src/frustration-plugin.ts Enables shadow mutation observation.
packages/plugin-autocapture-browser/src/data-extractor.ts Integrates shadow configuration and enrichment.
packages/plugin-autocapture-browser/src/autocapture-plugin.ts Wires the gate into plugin setup.
packages/plugin-autocapture-browser/SHADOW-DOM.md Documents architecture and limitations.
packages/plugin-autocapture-browser/README.md Links Shadow DOM documentation.
packages/plugin-autocapture-browser/e2e/shadow-dom.spec.ts Tests browser autocapture behavior.
packages/plugin-autocapture-browser/e2e/shadow-dom-perf.spec.ts Adds manual performance comparisons.
packages/element-selector/test/strategies/stable-id.test.ts Updates resolved-config fixture.
packages/element-selector/test/strategies/explicit-tracking-attribute.test.ts Updates resolved-config fixture.
packages/element-selector/test/shadow.test.ts Tests shadow selector primitives.
packages/element-selector/test/schema/element-selector-remote-config.schema.test.ts Tests new schema fields.
packages/element-selector/test/scenarios/off-path-differential.test.ts Verifies disabled-path compatibility.
packages/element-selector/test/orchestrator.test.ts Updates resolved-config fixture.
packages/element-selector/test/index.test.ts Tests public shadow exports.
packages/element-selector/test/helpers/get-stable-id.test.ts Updates resolved-config fixture.
packages/element-selector/test/fallback-css-path.test.ts Updates resolved-config fixture.
packages/element-selector/test/config/resolve-config.test.ts Tests shadow configuration resolution.
packages/element-selector/src/types.ts Defines shadow configuration fields.
packages/element-selector/src/index.ts Exports shadow traversal APIs.
packages/element-selector/src/helpers/shadow.ts Implements traversal and resolution.
packages/element-selector/src/fallback-css-path.ts Adds shadow-root path anchoring.
packages/element-selector/src/engine.ts Generates composite shadow selectors.
packages/element-selector/src/config/resolve-config.ts Resolves and clamps shadow settings.
packages/element-selector/schema/element-selector-remote-config.schema.json Defines remote-config schema fields.
packages/element-selector/README.md Documents selector Shadow DOM support.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/plugin-autocapture-browser/src/helpers.ts Outdated
Comment thread packages/plugin-autocapture-browser/src/data-extractor.ts Outdated
Comment thread test-server/shadow-dom-perf.html
Comment thread packages/plugin-autocapture-browser/src/autocapture-plugin.ts Outdated
Comment thread packages/element-selector/src/types.ts Outdated
@cely404 cely404 changed the title Sr 4788 zoning shadow dom support feat(zoning): add support for shadow dom Aug 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 40 out of 40 changed files in this pull request and generated 2 comments.

Suppressed comments (2)

packages/plugin-autocapture-browser/src/observables.ts:327

  • This descendant scan restarts maxDepth from every added node instead of accounting for the shadow depth of the mutation target. For example, with maxDepth: 1, a node added inside a depth-1 root can contain another shadow root, and this call observes matching depth-2 elements even though mutation discovery correctly refuses that root. Those elements can then produce exposure paths truncated to an ancestor, bypassing the configured budget. Carry the mutation's absolute/root depth into exposure discovery and scan only the remaining budget.
          observeMatchesInShadow(node);

playwright.config.ts:60

  • testIgnore is still applied when Playwright receives an explicit file filter, so the documented npx playwright test .../shadow-dom-perf.spec.ts command will report no tests instead of running this manual guard. Make this ignore conditional on an opt-in environment variable (and use it in the documented command), or provide a separate Playwright config that clears the ignore.

Comment thread packages/plugin-autocapture-browser/src/observables.ts Outdated
* @param selector A selector produced by the engine (may contain boundary delimiters).
*/
export function resolveSelector(root: ParentNode, selector: string): Element | null {
const segments = selector.split(SHADOW_BOUNDARY_DELIMITER);
@cely404
cely404 force-pushed the SR-4788-zoning-shadow-dom-support branch from 0365733 to a331327 Compare August 10, 2026 17:58
@cely404
cely404 marked this pull request as ready for review August 10, 2026 18:22
@cely404
cely404 requested a review from a team as a code owner August 10, 2026 18:22
@daniel-graham-amplitude

Copy link
Copy Markdown
Collaborator

bugbot run

@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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 60b9654. Configure here.

? querySelectorAllDeepInShadow(root, selectorString, shadow.maxDepth)
: querySelectorAllDeepLight(root, selectorString);

const querySelectorAllDeepLight = (root: Element | Document, selectorString: string): Element[] => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Would it work if we made a return type like:

type ElementList = Element[] || NodeListOf<Element>;

And then just have this function return the result of querySelectorAll without having to coerce it into a JS Array?

const shadow = readGate(shadowGate);
if (!shadow.enabled) {
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I noticed that you still observe for mutations even if !shadow.enabled. Is this because it's possible for shadow capture to be enabled later and we want the observer to be running when that happens?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Zoning added an exposure event as part of the initial launch months ago so we need to observe for mutations with or without shadow dom piercing. Mutations are still emitted we just skip attaching/discovering open shadow roots when the gate is off.

Rn we have a single observer lifecycle.
We could swap to separate observables when remote config lands, but that would mean tearing down and reconnecting observers. During the switch we may drop mutations.

Wdyt?

* `maxShadowDepth` crossings and by {@link MAX_SHADOW_DOM_TRAVERSAL_NODES}
* total nodes. Closed roots are invisible (`el.shadowRoot` is null) and skipped.
*/
export function collectOpenShadowRoots(root: Element, maxShadowDepth: number): OpenShadowRootEntry[] {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This function is the one addition to the code that gives me concerns about performance, being a depth first search traversal, it could cause some lengthy main thread blocking and add jank to our customers website.

Do you think it would be a good idea, not for the scope of this PR, if we added something like a shadowNodeCssSelector that, when provided by the customer, it identifies which nodes are shadow nodes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yea that sounds like a good idea.

My cursor agent also recommends a registry where we do a DFS scan once when remote config lands and then register new roots incrementally when addedNodes exposes them.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Okay, yeah let's leave it as a follow-up.

@Mercy811 Mercy811 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM! I thought some code changes are for zoning only and thought we could split this into a smaller PR for autocapture but looks like this is already the smallest PR.

One question: is shadowDomEnabled remote config public or it's on dynconf only?

@cely404

cely404 commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

LGTM! I thought some code changes are for zoning only and thought we could split this into a smaller PR for autocapture but looks like this is already the smallest PR.

One question: is shadowDomEnabled remote config public or it's on dynconf only?

Would you prefer I use stacked prs for this? Happy to do so if that is the preference. And as for rollout @jxiwang can you advise? I was planning to roll this out in the same way that the selector engine changes were rolled out.

- Cheaper `addedNodes` checks (e.g. `node.shadowRoot`) before DFS
- Batched discovery and more targeted exposure indexing

**Do not patch `attachShadow` by default.** Session replay (`@amplitude/rrweb-record`) and other libraries already wrap the same API; a global monkey patch is a last resort and needs dual-plugin validation (autocapture + session replay) before consideration.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@jxiwang

jxiwang commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

@cely404 I'd lean towards remote config settings for more flexibility in the future. Would this be something we enable by default in the future? I'm assuming not since there are some performance concerns.

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.

5 participants