@amplitude/element-selector

中文文档
0.3.3发布于 2026/11/07MITTypeScriptESM + CJS构建来源已验证近 30 天下载 515.6万

Shared element-selector algorithm consumed by autocapture, the Amplitude dashboard, and the Chrome extension visual tagger.

原始使用文档
文档目录23 节
  1. Why it exists
  2. Install
  3. Usage
  4. Standalone consumer
  5. Autocapture SDK plugin
  6. Chrome extension visual tagger
  7. How the algorithm works
  8. Strategy chain
  9. Fallback
  10. Sibling disambiguation: :nth-of-type , never classes
  11. Defaults
  12. enabled: false
  13. explicitTrackingAttribute: "data-amp-track-id"
  14. autogeneratedIdPatterns
  15. unstableClassPatterns
  16. maxAncestorWalkDepth: undefined
  17. Shadow DOM
  18. Rollout limitations (summary)
  19. Remote config
  20. Diagnostics
  21. Interactive testbed
  22. Testing
  23. Design references

@amplitude/element-selector

Shared element-selector algorithm consumed by the autocapture SDK plugin, the app.amplitude.com tagging UI, and the Chrome extension visual tagger. One implementation, three surfaces, identical output.

Why it exists

The legacy autocapture cssPath algorithm anchored on any element id it found, which broke selector stability whenever a framework generated an id like :r5: (React's useId) or radix-A1B2C3 (Radix UI). It also leaned heavily on classes for sibling disambiguation, which broke selector stability whenever a library applied transient state classes like swiper-slide-active or Mui-selected.

This package replaces that algorithm with a strategy-chain orchestrator that filters autogenerated ids and library state classes out of the anchor selection, then disambiguates siblings positionally via :nth-of-type. The result: selectors that survive component re-mounts, carousel interactions, hover/focus state churn, and minor design tweaks.

See the design doc at packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md for the full background and rationale.

Install

This package lives in the Amplitude-TypeScript monorepo and is published to npm as @amplitude/element-selector. Consumers in this repo declare it as a workspace dependency:

{
  "dependencies": {
    "@amplitude/element-selector": "workspace:*"
  }
}

The only runtime dependency is tslib; diagnostics accept any logger with warn and debug methods.

Usage

Standalone consumer

import { createSelectorEngine, resolveSelectorConfig } from '@amplitude/element-selector';

// Start with the documented defaults. Remote-config values can be applied as
// they arrive — `resolveSelectorConfig` is safe to call repeatedly.
const config = resolveSelectorConfig({ enabled: true });
const engine = createSelectorEngine(config);

// Generate a selector for any element. The engine returns the best selector
// it can produce, falling back to a positional walk when no strategy applies.
document.addEventListener('click', (event) => {
  const target = event.target as Element;
  const selector = engine.generate(target);
  console.log(`Clicked: ${selector}`);
});

Autocapture SDK plugin

The autocapture plugin instantiates one engine per init() call, threads its loggerProvider through for diagnostic logs, and forwards remote-config updates via updateConfig:

import { createSelectorEngine, resolveSelectorConfig } from '@amplitude/element-selector';

const engine = createSelectorEngine(resolveSelectorConfig(remote, config.loggerProvider), {
  logger: config.loggerProvider,
});

config.remoteConfigClient.subscribe(
  'configs.analyticsSDK.browserSDK.autocapture.elementSelector',
  'all',
  (next) => {
    engine.updateConfig(resolveSelectorConfig(next, config.loggerProvider));
  },
);

Chrome extension visual tagger

The extension reads the page's already-live engine from window.amplitude.elementSelector (when present) and subscribes to onConfigChange so its preview stays in sync with the customer's runtime config:

const pageEngine = (window as any).amplitude?.elementSelector;
if (pageEngine) {
  const unsubscribe = pageEngine.onConfigChange((nextConfig) => {
    rerenderTaggingUI(nextConfig);
  });
  // Call unsubscribe() on extension teardown.
}

How the algorithm works

The orchestrator walks from the clicked element up through its ancestors, trying each strategy at each level in priority order. The first strategy that produces a unique-resolving selector wins; the trail of elements between the anchor and the original target is expressed as a positional descent.

Strategy chain

OrderStrategyAnchor formatWhen it applies
1explicitTrackingAttribute[data-amp-track-id="value"]Customer set the tracking attribute with a non-empty value
2stableIdtag#idElement has an id, the id isn't suppressed, and it doesn't match any autogenerated-id pattern

Fallback

When neither strategy produces a unique selector anywhere on the walk, the engine invokes fallbackCssPath. The fallback walks from the target back up to <html>, expressing each step as tag:nth-of-type(n) and terminating early at the first unique id that survives the autogenerated-id filter.

Sibling disambiguation: :nth-of-type, never classes

The fallback does not use classes for sibling disambiguation. Classes are runtime-volatile in modern frameworks — Tailwind utilities change with design tweaks, CSS-in-JS hashes change every build, and library state classes (swiper-slide-active, is-open, Mui-selected) move between elements as the user interacts. Positional descent via :nth-of-type(n) is stable across all of these.

Defaults

enabled: false

v1 ships dormant. Autocapture continues to use the legacy cssPath until the remote-config payload flips enabled to true for an org.

explicitTrackingAttribute: "data-amp-track-id"

Customers can set this attribute on an element to anchor the selector on it explicitly. Setting it to an empty string is a suppression signal — the engine treats the element's id as if it didn't exist.

autogeneratedIdPatterns

The default list filters out ids that look generated rather than authored:

PatternCatchesExample
^:r[0-9a-z]+:$React useId:r5:, :rk:
^radix-Radix UI primitivesradix-A1B2C3
^headlessui-Headless UI componentsheadlessui-menu-button-:r5:
^mui-MUI internal id prefixmui-12345
^[a-f0-9]{16,}$ (i)UUIDs without hyphens, build hashesa9b8c7d6e5f4321b
-\d{8,}$Trailing long digit runssession-1700000000
\d{4,}Any 4+ consecutive digitsproduct-2134, row-1234

unstableClassPatterns

The default list (used by the fallback) catches classes that look stable in name but aren't stable in practice — Tailwind utilities, CSS-in-JS hashes (Emotion, styled-components, CSS modules, styled-jsx), and library runtime state classes (Swiper, MUI, Radix, BEM-style is-active).

maxAncestorWalkDepth: undefined

By default the walk runs all the way to <html>. Set this to a positive integer to cap the depth — useful for catastrophically deep DOMs where uniqueness checks could become expensive.

Shadow DOM

Shadow support is controlled independently from the selector engine kill switch via shadowDomEnabled and maxShadowDomDepth in remote config. Defaults: shadowDomEnabled: false, maxShadowDomDepth: 1 (clamped to [1, 10]).

When enabled, engine.generate emits a delimited selector (>>> between per-tree segments) that must be re-resolved with resolveSelector — plain document.querySelector cannot pierce shadow boundaries.

The autocapture plugin arms shadow support once per page via a latch (ShadowGate): the first remote-config delivery that enables it takes effect for the rest of the page load. A later delivery that disables shadow support does not turn it off until the next navigation.

Full design doc (architecture, performance, late-root scenarios, DFS mitigations, follow-ups): packages/plugin-autocapture-browser/SHADOW-DOM.md

Rollout limitations (summary)

LimitationImpact
Open shadow roots onlyClosed roots are opaque. composedPath() retargets events to the host; selectors cannot round-trip through closed roots.
Latch-once per pageDisabling shadowDomEnabled mid-session has no effect until the next full page load. First arming delivery also fixes maxShadowDomDepth for the page.
Late attachShadowRoots attached to elements already in the DOM are not discovered by observers. Clicks still capture; mutation/exposure may miss. A global attachShadow patch is not planned — session replay and other libs already wrap the same API (details).
Depth budgetTargets deeper than maxShadowDomDepth produce a best-effort selector for the outermost in-budget shadow host.
Traversal capsMAX_SHADOW_COMPOSED_WALK_ITERATIONS (ancestor walks) and MAX_SHADOW_DOM_TRAVERSAL_NODES (shadow-root collection DFS) bound synchronous work on pathological trees.

Pre-rollout: run e2e/shadow-dom.spec.ts (CI) and e2e/shadow-dom-perf.spec.ts locally before enabling the flag for an org.

Remote config

The remote-config payload shape is defined in src/types.ts as ElementSelectorRemoteConfig and mirrored as a JSON Schema at schema/element-selector-remote-config.schema.json for the dashboard's remote-config editor and backend validation.

Customer-provided pattern lists fully replace the defaults — they're not merged. An empty array (autogeneratedIdPatterns: []) is an explicit "disable filtering" signal, not a "use defaults" signal. Invalid regex strings are dropped (with a warn log) rather than crashing the engine.

Diagnostics

Every path that catches an error accepts an optional ElementSelectorLogger, which is structurally compatible with the SDK's @amplitude/analytics-core logger provider. When wired up, the engine surfaces:

  • warn — invalid regex strings in remote config, subscriber exceptions during updateConfig fan-out
  • debug — malformed selectors produced by a strategy

When no logger is provided, the engine stays silent — preserving fire-and-forget semantics for consumers that don't want diagnostic noise.

Interactive testbed

Run the package's testbed page locally to click around the engine against the canonical scenarios, edit remote-config payloads in real time, and watch the diagnostic logger output:

pnpm --filter @amplitude/element-selector testbed
# → builds the package, serves the testbed at http://localhost:4567

The testbed lives at packages/element-selector/testbed/ and imports the real built engine from lib/esm/index.js — there's no parallel implementation that can drift from the package. Every scenario from test/scenarios/ is rendered live; clicking any element shows the emitted selector, the strategy that won, and whether querySelector round-trips back to the clicked element.

Testing

pnpm --filter @amplitude/element-selector test
pnpm --filter @amplitude/element-selector lint
pnpm --filter @amplitude/element-selector build

The test suite includes:

  • Primitive unit tests for the pattern packs, helpers, strategies, config resolver, orchestrator, fallback, and engine factory
  • Schema drift guard — asserts the JSON Schema and the ElementSelectorRemoteConfig TypeScript type stay in sync
  • Scenarios corpus — a parameterized differential test that runs the engine over hand-picked fixtures (Swiper, MUI, Tailwind, semantic HTML, CSS-in-JS soup, etc.) and asserts both the expected selector and a querySelector round-trip
  • Property tests — generate random DOM trees with mixed stable / autogen ids and class soup, assert the engine always emits a selector that uniquely resolves back to the target

Design references

  • Design doc: packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md
  • Implementation plan: phased PR breakdown in the design doc's "Rollout" section

版本与发布记录

@amplitude/element-selector 共发布 18 个版本,首次发布于 2026/06/11;过去 12 个月发布了 18 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.3.3latest2026/11/071 天前
0.3.3-sr-bfcache-pageshow-snapshot.02026/11/052 天前
0.3.3-video-analytics-8.02026/10/2811 天前
0.3.3-video-analytics-7.02026/10/2712 天前
0.3.3-video-analytics-6.02026/10/1523 天前
0.3.22026/10/1028 天前

其他发布通道:alpha → 0.0.1,featzoning-selectors → 0.1.0,featelement-selector-v1 → 0.1.1-featelement-selector-v1.0,publish-sr-targeting → 0.2.0-publish-sr-targeting.0,element-path-v1 → 0.2.0-element-path-v1.0,video-analytics-2 → 0.3.1-video-analytics-2.0。

关于 @amplitude/element-selector 的常见问题

@amplitude/element-selector 是什么?

Shared element-selector algorithm consumed by autocapture, the Amplitude dashboard, and the Chrome extension visual tagger.

如何安装 @amplitude/element-selector?

在项目目录运行 npm install @amplitude/element-selector,或使用 pnpm add / yarn add / bun add。

@amplitude/element-selector 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@amplitude/element-selector 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@amplitude/element-selector 可以商用吗?

@amplitude/element-selector 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@amplitude/element-selector 的下载量有多少?

根据 npm 官方下载统计,@amplitude/element-selector 在 2026/10/06 – 2026/11/04 这 30 天内被下载 5,156,223 次,日均约 171,874 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@amplitude/element-selector 还在维护吗?

最新版本 0.3.3 发布于 2026/11/07(1 天前);至今共发布 18 个版本,其中过去 12 个月发布了 18 个;首次发布于 2026/06/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。