🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@amplitude/element-selector

Package Overview
Dependencies
Maintainers
6
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amplitude/element-selector

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

latest
Source
npmnpm
Version
0.2.1
Version published
Weekly downloads
236K
8.84%
Maintainers
6
Weekly downloads
 
Created
Source

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

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

FAQs

Package last updated on 15 Jul 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts