🎩 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
5
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amplitude/element-selector - npm Package Compare versions

Comparing version
0.1.1-featelement-selector-v1.0
to
0.2.0-element-path-v1.0
+1
-1
lib/cjs/engine.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAmB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAItE,MAAM,WAAW,2BAA2B;IAC1C,gHAAgH;IAChH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,8FAA8F;IAC9F,UAAU,CAAC,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;IAC/C;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,sBAAsB,EACrC,OAAO,GAAE,2BAAgC,GACxC,cAAc,CAoFhB"}
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAmB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAItE,MAAM,WAAW,2BAA2B;IAC1C,gHAAgH;IAChH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,8FAA8F;IAC9F,UAAU,CAAC,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;IAC/C;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,sBAAsB,EACrC,OAAO,GAAE,2BAAgC,GACxC,cAAc,CAqEhB"}

@@ -64,3 +64,3 @@ "use strict";

if (!config.enabled) {
return safeLegacyCssPath(el);
return (0, legacy_css_path_1.safeLegacyCssPath)(el, logger);
}

@@ -83,3 +83,3 @@ // Engine path: try the strategy chain, then the hardened fallback. Both

logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: strategy chain threw \u2014 falling back to legacy cssPath: ".concat(message));
return safeLegacyCssPath(el);
return (0, legacy_css_path_1.safeLegacyCssPath)(el, logger);
}

@@ -124,19 +124,4 @@ },

};
// Local wrapper so a thrown error in the legacy walker (extremely unlikely
// — Element inputs only — but defensible if a host environment is missing
// `CSS.escape` or `Node` constants) doesn't escape `generate` and crash the
// caller. Returns the empty string in that pathological case; consumers
// already coalesce empty results to a sentinel.
function safeLegacyCssPath(el) {
try {
return (0, legacy_css_path_1.legacyCssPath)(el);
}
catch (e) {
var message = e instanceof Error ? e.message : String(e);
logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: legacyCssPath threw \u2014 emitting empty selector: ".concat(message));
return '';
}
}
}
exports.createSelectorEngine = createSelectorEngine;
//# sourceMappingURL=engine.js.map

@@ -1,1 +0,1 @@

{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Engine factory — composes the strategy chain (via `runOrchestrator`) with\n * the safety-net fallback (`fallbackCssPath`) behind a single `SelectorEngine`\n * interface.\n *\n * This is the surface every consumer talks to:\n *\n * - autocapture SDK plugin → instantiates one engine per init() call,\n * wires it into the click handler, and forwards remote-config updates\n * via `updateConfig`.\n * - app.amplitude.com tagging UI → fetches the customer's remote config\n * out-of-band and stands up a transient engine to compute selectors for\n * elements the user clicks in the iframe.\n * - Chrome extension visual tagger → reads the customer's already-live\n * engine off `window.amplitude.elementSelector` (when present) and\n * subscribes to `onConfigChange` so the extension's preview stays in\n * sync with the customer's runtime config.\n *\n * The factory deliberately takes a pre-resolved `ResolvedSelectorConfig` rather\n * than the raw remote payload — config resolution is a separate concern handled\n * by `resolveSelectorConfig`, and keeping it out of the engine constructor lets\n * dashboard / extension consumers stand up an engine from a static snapshot\n * without re-running the full resolver.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ElementSelectorLogger, ResolvedSelectorConfig, SelectorEngine } from './types';\nimport { runOrchestrator, OrchestratorOptions } from './orchestrator';\nimport { fallbackCssPath } from './fallback-css-path';\nimport { legacyCssPath } from './legacy-css-path';\n\nexport interface CreateSelectorEngineOptions {\n /** Optional document or shadow root for uniqueness checks. Defaults per-call to the target's owner document. */\n scope?: ParentNode;\n /** Optional override of the strategy chain. Primarily for testing / dashboard ad-hoc runs. */\n strategies?: OrchestratorOptions['strategies'];\n /**\n * Optional logger threaded through the orchestrator and the subscriber-fan-out\n * inside `updateConfig`. When provided, the engine surfaces malformed\n * selectors (`debug`) and listener exceptions (`warn`); when absent it stays\n * silent — preserving the legacy \"fire-and-forget\" semantics existing\n * consumers may rely on.\n */\n logger?: ElementSelectorLogger;\n}\n\n/**\n * Build a `SelectorEngine` bound to the supplied config.\n *\n * The returned engine is independent — calling the factory twice yields two\n * engines with separate config state and separate subscriber lists. This is the\n * shape the autocapture plugin wants (one engine per SDK instance) and the\n * shape the Chrome extension consumes off the page (the extension reads, never\n * writes).\n */\nexport function createSelectorEngine(\n initialConfig: ResolvedSelectorConfig,\n options: CreateSelectorEngineOptions = {},\n): SelectorEngine {\n let config: ResolvedSelectorConfig = initialConfig;\n const subscribers = new Set<(config: ResolvedSelectorConfig) => void>();\n const logger = options.logger;\n\n const orchestratorOptions: OrchestratorOptions = {\n strategies: options.strategies,\n scope: options.scope,\n logger,\n };\n\n return {\n generate(el: Element): string {\n // Kill switch: when the customer's remote config has the engine\n // disabled (the default for orgs that haven't opted in), emit the same\n // Chromium-derived selector autocapture has shipped since before the\n // strategy chain existed. This is what makes \"flip enabled back to\n // false\" a one-config-fetch revert for both the SDK and the dashboard —\n // they both go through this entry point now.\n if (!config.enabled) {\n return safeLegacyCssPath(el);\n }\n\n // Engine path: try the strategy chain, then the hardened fallback. Both\n // are wrapped so a runtime exception (malformed config slipped past the\n // resolver, browser API quirks, a bad scope) still produces a usable\n // selector. Without this, an autocapture click handler could throw\n // mid-flow and the customer would silently lose the event — a worse\n // outcome than emitting a less-stable selector.\n try {\n const composed = runOrchestrator(el, config, orchestratorOptions);\n if (composed !== null) {\n return composed;\n }\n return fallbackCssPath(el, config, { scope: options.scope ?? el.ownerDocument ?? document });\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: strategy chain threw — falling back to legacy cssPath: ${message}`);\n return safeLegacyCssPath(el);\n }\n },\n\n getConfig(): Readonly<ResolvedSelectorConfig> {\n return config;\n },\n\n updateConfig(next: ResolvedSelectorConfig): void {\n config = next;\n // Notify subscribers in insertion order. Errors thrown by individual\n // subscribers are isolated so one bad listener can't break the others —\n // the extension's listener is the canonical consumer here and we don't\n // want SDK-side changes to cascade-fail because of an extension bug.\n for (const cb of subscribers) {\n try {\n cb(next);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: onConfigChange subscriber threw — ${message}`);\n }\n }\n },\n\n onConfigChange(cb: (config: ResolvedSelectorConfig) => void): () => void {\n subscribers.add(cb);\n return () => {\n subscribers.delete(cb);\n };\n },\n };\n\n // Local wrapper so a thrown error in the legacy walker (extremely unlikely\n // — Element inputs only — but defensible if a host environment is missing\n // `CSS.escape` or `Node` constants) doesn't escape `generate` and crash the\n // caller. Returns the empty string in that pathological case; consumers\n // already coalesce empty results to a sentinel.\n function safeLegacyCssPath(el: Element): string {\n try {\n return legacyCssPath(el);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: legacyCssPath threw — emitting empty selector: ${message}`);\n return '';\n }\n }\n}\n"]}
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;;;;AAGH,+CAAsE;AACtE,yDAAsD;AACtD,qDAAsD;AAiBtD;;;;;;;;GAQG;AACH,SAAgB,oBAAoB,CAClC,aAAqC,EACrC,OAAyC;IAAzC,wBAAA,EAAA,YAAyC;IAEzC,IAAI,MAAM,GAA2B,aAAa,CAAC;IACnD,IAAM,WAAW,GAAG,IAAI,GAAG,EAA4C,CAAC;IACxE,IAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAE9B,IAAM,mBAAmB,GAAwB;QAC/C,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,QAAA;KACP,CAAC;IAEF,OAAO;QACL,QAAQ,YAAC,EAAW;;YAClB,gEAAgE;YAChE,uEAAuE;YACvE,qEAAqE;YACrE,mEAAmE;YACnE,wEAAwE;YACxE,6CAA6C;YAC7C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE;gBACnB,OAAO,IAAA,mCAAiB,EAAC,EAAE,EAAE,MAAM,CAAC,CAAC;aACtC;YAED,wEAAwE;YACxE,wEAAwE;YACxE,qEAAqE;YACrE,mEAAmE;YACnE,oEAAoE;YACpE,gDAAgD;YAChD,IAAI;gBACF,IAAM,QAAQ,GAAG,IAAA,8BAAe,EAAC,EAAE,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAC;gBAClE,IAAI,QAAQ,KAAK,IAAI,EAAE;oBACrB,OAAO,QAAQ,CAAC;iBACjB;gBACD,OAAO,IAAA,mCAAe,EAAC,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAA,MAAA,OAAO,CAAC,KAAK,mCAAI,EAAE,CAAC,aAAa,mCAAI,QAAQ,EAAE,CAAC,CAAC;aAC9F;YAAC,OAAO,CAAC,EAAE;gBACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,mGAAuF,OAAO,CAAE,CAAC,CAAC;gBAC/G,OAAO,IAAA,mCAAiB,EAAC,EAAE,EAAE,MAAM,CAAC,CAAC;aACtC;QACH,CAAC;QAED,SAAS;YACP,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,YAAY,YAAC,IAA4B;;YACvC,MAAM,GAAG,IAAI,CAAC;;gBACd,qEAAqE;gBACrE,wEAAwE;gBACxE,uEAAuE;gBACvE,qEAAqE;gBACrE,KAAiB,IAAA,gBAAA,iBAAA,WAAW,CAAA,wCAAA,iEAAE;oBAAzB,IAAM,EAAE,wBAAA;oBACX,IAAI;wBACF,EAAE,CAAC,IAAI,CAAC,CAAC;qBACV;oBAAC,OAAO,CAAC,EAAE;wBACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;wBAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,8EAAkE,OAAO,CAAE,CAAC,CAAC;qBAC3F;iBACF;;;;;;;;;QACH,CAAC;QAED,cAAc,YAAC,EAA4C;YACzD,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACpB,OAAO;gBACL,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACzB,CAAC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AAxED,oDAwEC","sourcesContent":["/**\n * Engine factory — composes the strategy chain (via `runOrchestrator`) with\n * the safety-net fallback (`fallbackCssPath`) behind a single `SelectorEngine`\n * interface.\n *\n * This is the surface every consumer talks to:\n *\n * - autocapture SDK plugin → instantiates one engine per init() call,\n * wires it into the click handler, and forwards remote-config updates\n * via `updateConfig`.\n * - app.amplitude.com tagging UI → fetches the customer's remote config\n * out-of-band and stands up a transient engine to compute selectors for\n * elements the user clicks in the iframe.\n * - Chrome extension visual tagger → reads the customer's already-live\n * engine off `window.amplitude.elementSelector` (when present) and\n * subscribes to `onConfigChange` so the extension's preview stays in\n * sync with the customer's runtime config.\n *\n * The factory deliberately takes a pre-resolved `ResolvedSelectorConfig` rather\n * than the raw remote payload — config resolution is a separate concern handled\n * by `resolveSelectorConfig`, and keeping it out of the engine constructor lets\n * dashboard / extension consumers stand up an engine from a static snapshot\n * without re-running the full resolver.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ElementSelectorLogger, ResolvedSelectorConfig, SelectorEngine } from './types';\nimport { runOrchestrator, OrchestratorOptions } from './orchestrator';\nimport { fallbackCssPath } from './fallback-css-path';\nimport { safeLegacyCssPath } from './legacy-css-path';\n\nexport interface CreateSelectorEngineOptions {\n /** Optional document or shadow root for uniqueness checks. Defaults per-call to the target's owner document. */\n scope?: ParentNode;\n /** Optional override of the strategy chain. Primarily for testing / dashboard ad-hoc runs. */\n strategies?: OrchestratorOptions['strategies'];\n /**\n * Optional logger threaded through the orchestrator and the subscriber-fan-out\n * inside `updateConfig`. When provided, the engine surfaces malformed\n * selectors (`debug`) and listener exceptions (`warn`); when absent it stays\n * silent — preserving the legacy \"fire-and-forget\" semantics existing\n * consumers may rely on.\n */\n logger?: ElementSelectorLogger;\n}\n\n/**\n * Build a `SelectorEngine` bound to the supplied config.\n *\n * The returned engine is independent — calling the factory twice yields two\n * engines with separate config state and separate subscriber lists. This is the\n * shape the autocapture plugin wants (one engine per SDK instance) and the\n * shape the Chrome extension consumes off the page (the extension reads, never\n * writes).\n */\nexport function createSelectorEngine(\n initialConfig: ResolvedSelectorConfig,\n options: CreateSelectorEngineOptions = {},\n): SelectorEngine {\n let config: ResolvedSelectorConfig = initialConfig;\n const subscribers = new Set<(config: ResolvedSelectorConfig) => void>();\n const logger = options.logger;\n\n const orchestratorOptions: OrchestratorOptions = {\n strategies: options.strategies,\n scope: options.scope,\n logger,\n };\n\n return {\n generate(el: Element): string {\n // Kill switch: when the customer's remote config has the engine\n // disabled (the default for orgs that haven't opted in), emit the same\n // Chromium-derived selector autocapture has shipped since before the\n // strategy chain existed. This is what makes \"flip enabled back to\n // false\" a one-config-fetch revert for both the SDK and the dashboard —\n // they both go through this entry point now.\n if (!config.enabled) {\n return safeLegacyCssPath(el, logger);\n }\n\n // Engine path: try the strategy chain, then the hardened fallback. Both\n // are wrapped so a runtime exception (malformed config slipped past the\n // resolver, browser API quirks, a bad scope) still produces a usable\n // selector. Without this, an autocapture click handler could throw\n // mid-flow and the customer would silently lose the event — a worse\n // outcome than emitting a less-stable selector.\n try {\n const composed = runOrchestrator(el, config, orchestratorOptions);\n if (composed !== null) {\n return composed;\n }\n return fallbackCssPath(el, config, { scope: options.scope ?? el.ownerDocument ?? document });\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: strategy chain threw — falling back to legacy cssPath: ${message}`);\n return safeLegacyCssPath(el, logger);\n }\n },\n\n getConfig(): Readonly<ResolvedSelectorConfig> {\n return config;\n },\n\n updateConfig(next: ResolvedSelectorConfig): void {\n config = next;\n // Notify subscribers in insertion order. Errors thrown by individual\n // subscribers are isolated so one bad listener can't break the others —\n // the extension's listener is the canonical consumer here and we don't\n // want SDK-side changes to cascade-fail because of an extension bug.\n for (const cb of subscribers) {\n try {\n cb(next);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: onConfigChange subscriber threw — ${message}`);\n }\n }\n },\n\n onConfigChange(cb: (config: ResolvedSelectorConfig) => void): () => void {\n subscribers.add(cb);\n return () => {\n subscribers.delete(cb);\n };\n },\n };\n}\n"]}

@@ -1,1 +0,1 @@

{"version":3,"file":"generate-selector.d.ts","sourceRoot":"","sources":["../../src/generate-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGjE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gBAAgB,OACvB,OAAO,UACH,cAAc,GAAG,IAAI,UACrB,sBAAsB,KAC7B,MAiBF,CAAC"}
{"version":3,"file":"generate-selector.d.ts","sourceRoot":"","sources":["../../src/generate-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGjE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gBAAgB,OACvB,OAAO,UACH,cAAc,GAAG,IAAI,UACrB,sBAAsB,KAC7B,MAkBF,CAAC"}

@@ -64,14 +64,14 @@ "use strict";

// straight to the legacy walker — the same selector the SDK would be
// emitting if `config.enabled === false`. `config` is accepted for symmetry
// with the engine signature and so future versions can extend behavior
// without a breaking change.
// emitting if `config.enabled === false`.
//
// `config` is intentionally inert here: call sites with `engine: null`
// (boot-time Chrome extension, inert dashboard context) have no strategy
// engine to run. Passing `enabled: true` cannot take effect without a live
// engine — legacyCssPath is the only output available. `config` is accepted
// for signature symmetry with the engine-present branch and so future
// versions can extend behavior without a breaking change.
void config;
try {
return (0, legacy_css_path_1.legacyCssPath)(el);
}
catch (_a) {
return '';
}
return (0, legacy_css_path_1.safeLegacyCssPath)(el);
};
exports.generateSelector = generateSelector;
//# sourceMappingURL=generate-selector.js.map

@@ -1,1 +0,1 @@

{"version":3,"file":"generate-selector.js","sourceRoot":"","sources":["../../src/generate-selector.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;;;AAGH,qDAAkD;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACI,IAAM,gBAAgB,GAAG,UAC9B,EAAW,EACX,MAA6B,EAC7B,MAA8B;IAE9B,IAAI,MAAM,EAAE;QACV,mEAAmE;QACnE,8DAA8D;QAC9D,OAAO,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;KAC5B;IACD,0EAA0E;IAC1E,qEAAqE;IACrE,4EAA4E;IAC5E,uEAAuE;IACvE,6BAA6B;IAC7B,KAAK,MAAM,CAAC;IACZ,IAAI;QACF,OAAO,IAAA,+BAAa,EAAC,EAAE,CAAC,CAAC;KAC1B;IAAC,WAAM;QACN,OAAO,EAAE,CAAC;KACX;AACH,CAAC,CAAC;AArBW,QAAA,gBAAgB,oBAqB3B","sourcesContent":["/**\n * Top-level chokepoint for \"generate a selector for this element.\"\n *\n * Both the SDK plugin (`plugin-autocapture-browser`) and the dashboard\n * (`session-replay-ui`) used to maintain their own copy of this routing logic:\n *\n * if (config.enabled && engine) return engine.generate(el);\n * else return cssPath(el, false);\n *\n * Two consumers, two routers, two locally-bundled copies of `cssPath`. As soon\n * as we considered a third consumer (Chrome extension visual tagger, future\n * tagging surfaces) the duplication became untenable — bug fixes had to land\n * three places and the kill-switch semantics could drift between them.\n *\n * Moving the kill switch inside `engine.generate` covered the \"engine present\"\n * case. This helper covers the remaining \"engine is null\" case that some\n * consumers genuinely have:\n *\n * - dashboard's `useElementSelectorEngineContext()` returns\n * `{ engine: null }` when no provider is mounted (tests, content-renderer\n * paths, future call sites outside the canonical mount tree).\n * - Chrome extension boots before the customer's SDK has stood up an engine\n * on the page; reads `null` from `window.amplitude.elementSelector` until\n * the SDK init finishes.\n *\n * In those cases, callers want the legacy `cssPath` selector — same shape the\n * SDK is emitting in production for orgs without the engine. This helper makes\n * that explicit and centralizes the rule so future consumers don't reinvent\n * it.\n */\n\nimport { ResolvedSelectorConfig, SelectorEngine } from './types';\nimport { legacyCssPath } from './legacy-css-path';\n\n/**\n * Generate a selector for `el`, transparently routing to the legacy\n * `cssPath` walker when no engine is available.\n *\n * Why this signature (engine + config separately, rather than just engine):\n * the dashboard's inert context has `engine: null` but still carries a\n * resolved config. Both `null`-engine and engine-with-`enabled: false`\n * collapse to \"legacy `cssPath`\", and we want the same one-line call site\n * to handle both. When the engine *is* present, this just delegates — the\n * engine's own kill switch handles `enabled === false`.\n *\n * Never throws. Returns the empty string in the pathological case where the\n * legacy walker itself can't produce one (non-element nodes; environments\n * missing `CSS.escape`).\n *\n * @param el DOM element to identify.\n * @param engine Live `SelectorEngine`, or `null` when no engine is mounted.\n * @param config Resolved selector config. Only consulted in the null-engine\n * branch; when `engine` is present, the engine consults its\n * own internal config.\n */\nexport const generateSelector = (\n el: Element,\n engine: SelectorEngine | null,\n config: ResolvedSelectorConfig,\n): string => {\n if (engine) {\n // The engine has its own kill switch (`config.enabled === false` →\n // legacy cssPath) and its own try/catch safety net. Trust it.\n return engine.generate(el);\n }\n // Engine-null branch. We don't have a live engine to consult, so we route\n // straight to the legacy walker — the same selector the SDK would be\n // emitting if `config.enabled === false`. `config` is accepted for symmetry\n // with the engine signature and so future versions can extend behavior\n // without a breaking change.\n void config;\n try {\n return legacyCssPath(el);\n } catch {\n return '';\n }\n};\n"]}
{"version":3,"file":"generate-selector.js","sourceRoot":"","sources":["../../src/generate-selector.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;;;AAGH,qDAAsD;AAEtD;;;;;;;;;;;;;;;;;;;;GAoBG;AACI,IAAM,gBAAgB,GAAG,UAC9B,EAAW,EACX,MAA6B,EAC7B,MAA8B;IAE9B,IAAI,MAAM,EAAE;QACV,mEAAmE;QACnE,8DAA8D;QAC9D,OAAO,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;KAC5B;IACD,0EAA0E;IAC1E,qEAAqE;IACrE,0CAA0C;IAC1C,EAAE;IACF,uEAAuE;IACvE,yEAAyE;IACzE,2EAA2E;IAC3E,4EAA4E;IAC5E,sEAAsE;IACtE,0DAA0D;IAC1D,KAAK,MAAM,CAAC;IACZ,OAAO,IAAA,mCAAiB,EAAC,EAAE,CAAC,CAAC;AAC/B,CAAC,CAAC;AAtBW,QAAA,gBAAgB,oBAsB3B","sourcesContent":["/**\n * Top-level chokepoint for \"generate a selector for this element.\"\n *\n * Both the SDK plugin (`plugin-autocapture-browser`) and the dashboard\n * (`session-replay-ui`) used to maintain their own copy of this routing logic:\n *\n * if (config.enabled && engine) return engine.generate(el);\n * else return cssPath(el, false);\n *\n * Two consumers, two routers, two locally-bundled copies of `cssPath`. As soon\n * as we considered a third consumer (Chrome extension visual tagger, future\n * tagging surfaces) the duplication became untenable — bug fixes had to land\n * three places and the kill-switch semantics could drift between them.\n *\n * Moving the kill switch inside `engine.generate` covered the \"engine present\"\n * case. This helper covers the remaining \"engine is null\" case that some\n * consumers genuinely have:\n *\n * - dashboard's `useElementSelectorEngineContext()` returns\n * `{ engine: null }` when no provider is mounted (tests, content-renderer\n * paths, future call sites outside the canonical mount tree).\n * - Chrome extension boots before the customer's SDK has stood up an engine\n * on the page; reads `null` from `window.amplitude.elementSelector` until\n * the SDK init finishes.\n *\n * In those cases, callers want the legacy `cssPath` selector — same shape the\n * SDK is emitting in production for orgs without the engine. This helper makes\n * that explicit and centralizes the rule so future consumers don't reinvent\n * it.\n */\n\nimport { ResolvedSelectorConfig, SelectorEngine } from './types';\nimport { safeLegacyCssPath } from './legacy-css-path';\n\n/**\n * Generate a selector for `el`, transparently routing to the legacy\n * `cssPath` walker when no engine is available.\n *\n * Why this signature (engine + config separately, rather than just engine):\n * the dashboard's inert context has `engine: null` but still carries a\n * resolved config. Both `null`-engine and engine-with-`enabled: false`\n * collapse to \"legacy `cssPath`\", and we want the same one-line call site\n * to handle both. When the engine *is* present, this just delegates — the\n * engine's own kill switch handles `enabled === false`.\n *\n * Never throws. Returns the empty string in the pathological case where the\n * legacy walker itself can't produce one (non-element nodes; environments\n * missing `CSS.escape`).\n *\n * @param el DOM element to identify.\n * @param engine Live `SelectorEngine`, or `null` when no engine is mounted.\n * @param config Resolved selector config. Only consulted in the null-engine\n * branch; when `engine` is present, the engine consults its\n * own internal config.\n */\nexport const generateSelector = (\n el: Element,\n engine: SelectorEngine | null,\n config: ResolvedSelectorConfig,\n): string => {\n if (engine) {\n // The engine has its own kill switch (`config.enabled === false` →\n // legacy cssPath) and its own try/catch safety net. Trust it.\n return engine.generate(el);\n }\n // Engine-null branch. We don't have a live engine to consult, so we route\n // straight to the legacy walker — the same selector the SDK would be\n // emitting if `config.enabled === false`.\n //\n // `config` is intentionally inert here: call sites with `engine: null`\n // (boot-time Chrome extension, inert dashboard context) have no strategy\n // engine to run. Passing `enabled: true` cannot take effect without a live\n // engine — legacyCssPath is the only output available. `config` is accepted\n // for signature symmetry with the engine-present branch and so future\n // versions can extend behavior without a breaking change.\n void config;\n return safeLegacyCssPath(el);\n};\n"]}

@@ -72,2 +72,3 @@ /**

*/
import { ElementSelectorLogger } from './types';
/**

@@ -85,2 +86,10 @@ * Produce a CSS selector string for `node` using the legacy positional walker.

export declare const legacyCssPath: (node: Element, optimized?: boolean) => string;
/**
* Invoke `legacyCssPath` without letting a throw escape to the caller.
*
* Shared by `engine.generate` (kill switch + strategy-chain safety net) and
* `generateSelector` (null-engine branch) so the swallow-warn-fallback
* guarantee lives in one place.
*/
export declare function safeLegacyCssPath(el: Element, logger?: ElementSelectorLogger): string;
//# sourceMappingURL=legacy-css-path.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"legacy-css-path.d.ts","sourceRoot":"","sources":["../../src/legacy-css-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAgBH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,SAAU,OAAO,cAAc,OAAO,KAAG,MAuBlE,CAAC"}
{"version":3,"file":"legacy-css-path.d.ts","sourceRoot":"","sources":["../../src/legacy-css-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAOH,OAAO,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAUhD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,SAAU,OAAO,cAAc,OAAO,KAAG,MAuBlE,CAAC;AAmHF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAQrF"}

@@ -74,8 +74,4 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.legacyCssPath = void 0;
exports.safeLegacyCssPath = exports.legacyCssPath = void 0;
var tslib_1 = require("tslib");
/* istanbul ignore file -- the algorithm is locked-down behavior inherited from
Chromium and validated by the SDK's own end-to-end suite. Re-running line
coverage here only obscures the parts of the package that *do* have
meaningful coverage. */
var escape_css_identifier_1 = require("./helpers/escape-css-identifier");

@@ -231,2 +227,20 @@ var Step = /** @class */ (function () {

var idSelector = function (id) { return '#' + (0, escape_css_identifier_1.escapeCssIdentifier)(id); };
/**
* Invoke `legacyCssPath` without letting a throw escape to the caller.
*
* Shared by `engine.generate` (kill switch + strategy-chain safety net) and
* `generateSelector` (null-engine branch) so the swallow-warn-fallback
* guarantee lives in one place.
*/
function safeLegacyCssPath(el, logger) {
try {
return (0, exports.legacyCssPath)(el);
}
catch (e) {
var message = e instanceof Error ? e.message : String(e);
logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: legacyCssPath threw \u2014 emitting empty selector: ".concat(message));
return '';
}
}
exports.safeLegacyCssPath = safeLegacyCssPath;
//# sourceMappingURL=legacy-css-path.js.map

@@ -1,1 +0,1 @@

{"version":3,"file":"legacy-css-path.js","sourceRoot":"","sources":["../../src/legacy-css-path.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Canonical legacy CSS-path walker.\n *\n * This is the Chromium-DevTools-derived `cssPath` algorithm that was the only\n * selector source autocapture used before the strategy engine existed. It still\n * matters in two situations:\n *\n * 1. The kill switch: when `ResolvedSelectorConfig.enabled === false`, the\n * engine routes here so a customer who flips the engine off mid-session\n * keeps emitting the same selectors their downstream zones and cohorts\n * were built against.\n * 2. The safety net: when the strategy chain or its internal walker throws\n * anything unexpected, the engine catches and falls back here. The\n * autocapture click handler must always produce *some* selector — a\n * runtime exception in selector generation is a worse outcome than a\n * slightly less stable selector.\n *\n * Until this file existed, the algorithm was duplicated in two places kept\n * \"in sync by comment\":\n *\n * - `packages/plugin-autocapture-browser/src/libs/element-path.ts`\n * - `javascript/packages/session-replay-ui/src/utils/element-path.ts`\n *\n * Both will eventually re-export from here so divergence becomes impossible.\n *\n * Behavior is byte-identical to those two copies — selectors emitted by this\n * function for a given element MUST match what the SDK has been emitting in\n * production. Do not \"fix\" the selector format without an explicit version\n * bump and a coordinated dashboard rollout.\n *\n * **Why this lives in the package, not as an optional consumer concern:**\n *\n * Every consumer that uses `engine.generate(el)` benefits from the kill\n * switch and the safety net automatically. Without this, each new consumer\n * (Chrome extension visual tagger, future tagging surfaces) has to\n * reimplement the same router and the same legacy walker. We learned that\n * the hard way — by writing the same `if (config.enabled && engine) ...\n * else cssPath()` block in two repositories with two copies of cssPath.\n *\n * Code is adapted from The Chromium Authors.\n * Source: https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/panels/elements/DOMPath.ts#L14\n * License: BSD-style license\n *\n * Copyright 2014 The Chromium Authors\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are\n * met:\n *\n * * Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * * Redistributions in binary form must reproduce the above\n * copyright notice, this list of conditions and the following\n * disclaimer in the documentation and/or other materials provided\n * with the distribution.\n * * Neither the name of Google Inc. nor the names of its\n * contributors may be used to endorse or promote products derived\n * from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n * \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n\n/* istanbul ignore file -- the algorithm is locked-down behavior inherited from\n Chromium and validated by the SDK's own end-to-end suite. Re-running line\n coverage here only obscures the parts of the package that *do* have\n meaningful coverage. */\n\nimport { escapeCssIdentifier } from './helpers/escape-css-identifier';\n\nclass Step {\n constructor(public value: string, public optimized: boolean) {}\n toString(): string {\n return this.value;\n }\n}\n\n/**\n * Produce a CSS selector string for `node` using the legacy positional walker.\n *\n * Returns the empty string for non-element nodes — same shape every existing\n * caller depends on. Never throws for well-formed Element inputs.\n *\n * @param node Element to identify.\n * @param optimized When `true`, short-circuits at the nearest unique-ish step\n * (id or `<body>` / `<head>` / `<html>`). Default `false`,\n * which matches what autocapture has always passed.\n */\nexport const legacyCssPath = (node: Element, optimized?: boolean): string => {\n // `node` is already an Element; this check is defensive.\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const steps: Step[] = [];\n let contextNode: Element | null = node;\n\n while (contextNode) {\n const step = cssPathStep(contextNode, Boolean(optimized), contextNode === node);\n if (!step) {\n break;\n }\n steps.push(step);\n if (step.optimized) {\n break;\n }\n contextNode = contextNode.parentElement;\n }\n\n steps.reverse();\n return steps.join(' > ');\n};\n\nconst cssPathStep = (node: Element, optimized: boolean, isTargetNode: boolean): Step | null => {\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return null;\n }\n\n const id = node.getAttribute('id');\n if (optimized) {\n if (id) {\n return new Step(idSelector(id), true);\n }\n\n const nodeNameLower = node.tagName.toLowerCase();\n if (nodeNameLower === 'body' || nodeNameLower === 'head' || nodeNameLower === 'html') {\n return new Step(nodeNameLower, true);\n }\n }\n\n const nodeName = node.tagName.toLowerCase();\n\n if (id) {\n return new Step(nodeName + idSelector(id), true);\n }\n\n const parent = node.parentNode;\n if (!parent || parent.nodeType === Node.DOCUMENT_NODE) {\n return new Step(nodeName, true);\n }\n\n const prefixedOwnClassNamesArray = prefixedElementClassNames(node);\n let needsClassNames = false;\n let needsNthChild = false;\n let ownIndex = -1;\n let elementIndex = -1;\n\n const siblings: HTMLCollectionOf<Element> = (parent as Element | Document).children;\n\n for (let i = 0; siblings && (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {\n const sibling = siblings[i];\n if (sibling.nodeType !== Node.ELEMENT_NODE) {\n continue;\n }\n\n elementIndex += 1;\n if (sibling === node) {\n ownIndex = elementIndex;\n continue;\n }\n if (needsNthChild) {\n continue;\n }\n\n if (sibling.tagName.toLowerCase() !== nodeName) {\n continue;\n }\n\n needsClassNames = true;\n const ownClassNames = new Set<string>(prefixedOwnClassNamesArray);\n if (!ownClassNames.size) {\n needsNthChild = true;\n continue;\n }\n\n const siblingClassNamesArray = prefixedElementClassNames(sibling);\n for (let j = 0; j < siblingClassNamesArray.length; ++j) {\n const siblingClass = siblingClassNamesArray[j];\n if (!ownClassNames.has(siblingClass)) {\n continue;\n }\n ownClassNames.delete(siblingClass);\n if (!ownClassNames.size) {\n needsNthChild = true;\n break;\n }\n }\n }\n\n let result = nodeName;\n if (\n isTargetNode &&\n nodeName.toLowerCase() === 'input' &&\n node.getAttribute('type') &&\n !node.getAttribute('id') &&\n !node.getAttribute('class')\n ) {\n result += '[type=' + escapeCssIdentifier(node.getAttribute('type') || '') + ']';\n }\n if (needsNthChild) {\n result += ':nth-child(' + String(ownIndex + 1) + ')';\n } else if (needsClassNames) {\n for (const prefixedName of prefixedOwnClassNamesArray) {\n result += '.' + escapeCssIdentifier(prefixedName.slice(1));\n }\n }\n\n return new Step(result, false);\n};\n\nconst prefixedElementClassNames = (el: Element): string[] => {\n const classAttribute = el.getAttribute('class');\n if (!classAttribute) {\n return [];\n }\n return classAttribute\n .split(/\\s+/g)\n .filter(Boolean)\n .map((name) => {\n // The prefix is required to store \"__proto__\" in a object-based map.\n return '$' + name;\n });\n};\n\nconst idSelector = (id: string): string => '#' + escapeCssIdentifier(id);\n"]}
{"version":3,"file":"legacy-css-path.js","sourceRoot":"","sources":["../../src/legacy-css-path.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Canonical legacy CSS-path walker.\n *\n * This is the Chromium-DevTools-derived `cssPath` algorithm that was the only\n * selector source autocapture used before the strategy engine existed. It still\n * matters in two situations:\n *\n * 1. The kill switch: when `ResolvedSelectorConfig.enabled === false`, the\n * engine routes here so a customer who flips the engine off mid-session\n * keeps emitting the same selectors their downstream zones and cohorts\n * were built against.\n * 2. The safety net: when the strategy chain or its internal walker throws\n * anything unexpected, the engine catches and falls back here. The\n * autocapture click handler must always produce *some* selector — a\n * runtime exception in selector generation is a worse outcome than a\n * slightly less stable selector.\n *\n * Until this file existed, the algorithm was duplicated in two places kept\n * \"in sync by comment\":\n *\n * - `packages/plugin-autocapture-browser/src/libs/element-path.ts`\n * - `javascript/packages/session-replay-ui/src/utils/element-path.ts`\n *\n * Both will eventually re-export from here so divergence becomes impossible.\n *\n * Behavior is byte-identical to those two copies — selectors emitted by this\n * function for a given element MUST match what the SDK has been emitting in\n * production. Do not \"fix\" the selector format without an explicit version\n * bump and a coordinated dashboard rollout.\n *\n * **Why this lives in the package, not as an optional consumer concern:**\n *\n * Every consumer that uses `engine.generate(el)` benefits from the kill\n * switch and the safety net automatically. Without this, each new consumer\n * (Chrome extension visual tagger, future tagging surfaces) has to\n * reimplement the same router and the same legacy walker. We learned that\n * the hard way — by writing the same `if (config.enabled && engine) ...\n * else cssPath()` block in two repositories with two copies of cssPath.\n *\n * Code is adapted from The Chromium Authors.\n * Source: https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/panels/elements/DOMPath.ts#L14\n * License: BSD-style license\n *\n * Copyright 2014 The Chromium Authors\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are\n * met:\n *\n * * Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * * Redistributions in binary form must reproduce the above\n * copyright notice, this list of conditions and the following\n * disclaimer in the documentation and/or other materials provided\n * with the distribution.\n * * Neither the name of Google Inc. nor the names of its\n * contributors may be used to endorse or promote products derived\n * from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n * \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n\n/* istanbul ignore file -- the algorithm is locked-down behavior inherited from\n Chromium and validated by the SDK's own end-to-end suite. Re-running line\n coverage here only obscures the parts of the package that *do* have\n meaningful coverage. */\n\nimport { ElementSelectorLogger } from './types';\nimport { escapeCssIdentifier } from './helpers/escape-css-identifier';\n\nclass Step {\n constructor(public value: string, public optimized: boolean) {}\n toString(): string {\n return this.value;\n }\n}\n\n/**\n * Produce a CSS selector string for `node` using the legacy positional walker.\n *\n * Returns the empty string for non-element nodes — same shape every existing\n * caller depends on. Never throws for well-formed Element inputs.\n *\n * @param node Element to identify.\n * @param optimized When `true`, short-circuits at the nearest unique-ish step\n * (id or `<body>` / `<head>` / `<html>`). Default `false`,\n * which matches what autocapture has always passed.\n */\nexport const legacyCssPath = (node: Element, optimized?: boolean): string => {\n // `node` is already an Element; this check is defensive.\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const steps: Step[] = [];\n let contextNode: Element | null = node;\n\n while (contextNode) {\n const step = cssPathStep(contextNode, Boolean(optimized), contextNode === node);\n if (!step) {\n break;\n }\n steps.push(step);\n if (step.optimized) {\n break;\n }\n contextNode = contextNode.parentElement;\n }\n\n steps.reverse();\n return steps.join(' > ');\n};\n\nconst cssPathStep = (node: Element, optimized: boolean, isTargetNode: boolean): Step | null => {\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return null;\n }\n\n const id = node.getAttribute('id');\n if (optimized) {\n if (id) {\n return new Step(idSelector(id), true);\n }\n\n const nodeNameLower = node.tagName.toLowerCase();\n if (nodeNameLower === 'body' || nodeNameLower === 'head' || nodeNameLower === 'html') {\n return new Step(nodeNameLower, true);\n }\n }\n\n const nodeName = node.tagName.toLowerCase();\n\n if (id) {\n return new Step(nodeName + idSelector(id), true);\n }\n\n const parent = node.parentNode;\n if (!parent || parent.nodeType === Node.DOCUMENT_NODE) {\n return new Step(nodeName, true);\n }\n\n const prefixedOwnClassNamesArray = prefixedElementClassNames(node);\n let needsClassNames = false;\n let needsNthChild = false;\n let ownIndex = -1;\n let elementIndex = -1;\n\n const siblings: HTMLCollectionOf<Element> = (parent as Element | Document).children;\n\n for (let i = 0; siblings && (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {\n const sibling = siblings[i];\n if (sibling.nodeType !== Node.ELEMENT_NODE) {\n continue;\n }\n\n elementIndex += 1;\n if (sibling === node) {\n ownIndex = elementIndex;\n continue;\n }\n if (needsNthChild) {\n continue;\n }\n\n if (sibling.tagName.toLowerCase() !== nodeName) {\n continue;\n }\n\n needsClassNames = true;\n const ownClassNames = new Set<string>(prefixedOwnClassNamesArray);\n if (!ownClassNames.size) {\n needsNthChild = true;\n continue;\n }\n\n const siblingClassNamesArray = prefixedElementClassNames(sibling);\n for (let j = 0; j < siblingClassNamesArray.length; ++j) {\n const siblingClass = siblingClassNamesArray[j];\n if (!ownClassNames.has(siblingClass)) {\n continue;\n }\n ownClassNames.delete(siblingClass);\n if (!ownClassNames.size) {\n needsNthChild = true;\n break;\n }\n }\n }\n\n let result = nodeName;\n if (\n isTargetNode &&\n nodeName.toLowerCase() === 'input' &&\n node.getAttribute('type') &&\n !node.getAttribute('id') &&\n !node.getAttribute('class')\n ) {\n result += '[type=' + escapeCssIdentifier(node.getAttribute('type') || '') + ']';\n }\n if (needsNthChild) {\n result += ':nth-child(' + String(ownIndex + 1) + ')';\n } else if (needsClassNames) {\n for (const prefixedName of prefixedOwnClassNamesArray) {\n result += '.' + escapeCssIdentifier(prefixedName.slice(1));\n }\n }\n\n return new Step(result, false);\n};\n\nconst prefixedElementClassNames = (el: Element): string[] => {\n const classAttribute = el.getAttribute('class');\n if (!classAttribute) {\n return [];\n }\n return classAttribute\n .split(/\\s+/g)\n .filter(Boolean)\n .map((name) => {\n // The prefix is required to store \"__proto__\" in a object-based map.\n return '$' + name;\n });\n};\n\nconst idSelector = (id: string): string => '#' + escapeCssIdentifier(id);\n\n/**\n * Invoke `legacyCssPath` without letting a throw escape to the caller.\n *\n * Shared by `engine.generate` (kill switch + strategy-chain safety net) and\n * `generateSelector` (null-engine branch) so the swallow-warn-fallback\n * guarantee lives in one place.\n */\nexport function safeLegacyCssPath(el: Element, logger?: ElementSelectorLogger): string {\n try {\n return legacyCssPath(el);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: legacyCssPath threw — emitting empty selector: ${message}`);\n return '';\n }\n}\n"]}

@@ -1,1 +0,1 @@

{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAmB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAItE,MAAM,WAAW,2BAA2B;IAC1C,gHAAgH;IAChH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,8FAA8F;IAC9F,UAAU,CAAC,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;IAC/C;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,sBAAsB,EACrC,OAAO,GAAE,2BAAgC,GACxC,cAAc,CAoFhB"}
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAmB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAItE,MAAM,WAAW,2BAA2B;IAC1C,gHAAgH;IAChH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,8FAA8F;IAC9F,UAAU,CAAC,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;IAC/C;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,sBAAsB,EACrC,OAAO,GAAE,2BAAgC,GACxC,cAAc,CAqEhB"}

@@ -31,3 +31,3 @@ /**

import { fallbackCssPath } from './fallback-css-path';
import { legacyCssPath } from './legacy-css-path';
import { safeLegacyCssPath } from './legacy-css-path';
/**

@@ -62,3 +62,3 @@ * Build a `SelectorEngine` bound to the supplied config.

if (!config.enabled) {
return safeLegacyCssPath(el);
return safeLegacyCssPath(el, logger);
}

@@ -81,3 +81,3 @@ // Engine path: try the strategy chain, then the hardened fallback. Both

logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: strategy chain threw \u2014 falling back to legacy cssPath: ".concat(message));
return safeLegacyCssPath(el);
return safeLegacyCssPath(el, logger);
}

@@ -122,18 +122,3 @@ },

};
// Local wrapper so a thrown error in the legacy walker (extremely unlikely
// — Element inputs only — but defensible if a host environment is missing
// `CSS.escape` or `Node` constants) doesn't escape `generate` and crash the
// caller. Returns the empty string in that pathological case; consumers
// already coalesce empty results to a sentinel.
function safeLegacyCssPath(el) {
try {
return legacyCssPath(el);
}
catch (e) {
var message = e instanceof Error ? e.message : String(e);
logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: legacyCssPath threw \u2014 emitting empty selector: ".concat(message));
return '';
}
}
}
//# sourceMappingURL=engine.js.map

@@ -1,1 +0,1 @@

{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Engine factory — composes the strategy chain (via `runOrchestrator`) with\n * the safety-net fallback (`fallbackCssPath`) behind a single `SelectorEngine`\n * interface.\n *\n * This is the surface every consumer talks to:\n *\n * - autocapture SDK plugin → instantiates one engine per init() call,\n * wires it into the click handler, and forwards remote-config updates\n * via `updateConfig`.\n * - app.amplitude.com tagging UI → fetches the customer's remote config\n * out-of-band and stands up a transient engine to compute selectors for\n * elements the user clicks in the iframe.\n * - Chrome extension visual tagger → reads the customer's already-live\n * engine off `window.amplitude.elementSelector` (when present) and\n * subscribes to `onConfigChange` so the extension's preview stays in\n * sync with the customer's runtime config.\n *\n * The factory deliberately takes a pre-resolved `ResolvedSelectorConfig` rather\n * than the raw remote payload — config resolution is a separate concern handled\n * by `resolveSelectorConfig`, and keeping it out of the engine constructor lets\n * dashboard / extension consumers stand up an engine from a static snapshot\n * without re-running the full resolver.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ElementSelectorLogger, ResolvedSelectorConfig, SelectorEngine } from './types';\nimport { runOrchestrator, OrchestratorOptions } from './orchestrator';\nimport { fallbackCssPath } from './fallback-css-path';\nimport { legacyCssPath } from './legacy-css-path';\n\nexport interface CreateSelectorEngineOptions {\n /** Optional document or shadow root for uniqueness checks. Defaults per-call to the target's owner document. */\n scope?: ParentNode;\n /** Optional override of the strategy chain. Primarily for testing / dashboard ad-hoc runs. */\n strategies?: OrchestratorOptions['strategies'];\n /**\n * Optional logger threaded through the orchestrator and the subscriber-fan-out\n * inside `updateConfig`. When provided, the engine surfaces malformed\n * selectors (`debug`) and listener exceptions (`warn`); when absent it stays\n * silent — preserving the legacy \"fire-and-forget\" semantics existing\n * consumers may rely on.\n */\n logger?: ElementSelectorLogger;\n}\n\n/**\n * Build a `SelectorEngine` bound to the supplied config.\n *\n * The returned engine is independent — calling the factory twice yields two\n * engines with separate config state and separate subscriber lists. This is the\n * shape the autocapture plugin wants (one engine per SDK instance) and the\n * shape the Chrome extension consumes off the page (the extension reads, never\n * writes).\n */\nexport function createSelectorEngine(\n initialConfig: ResolvedSelectorConfig,\n options: CreateSelectorEngineOptions = {},\n): SelectorEngine {\n let config: ResolvedSelectorConfig = initialConfig;\n const subscribers = new Set<(config: ResolvedSelectorConfig) => void>();\n const logger = options.logger;\n\n const orchestratorOptions: OrchestratorOptions = {\n strategies: options.strategies,\n scope: options.scope,\n logger,\n };\n\n return {\n generate(el: Element): string {\n // Kill switch: when the customer's remote config has the engine\n // disabled (the default for orgs that haven't opted in), emit the same\n // Chromium-derived selector autocapture has shipped since before the\n // strategy chain existed. This is what makes \"flip enabled back to\n // false\" a one-config-fetch revert for both the SDK and the dashboard —\n // they both go through this entry point now.\n if (!config.enabled) {\n return safeLegacyCssPath(el);\n }\n\n // Engine path: try the strategy chain, then the hardened fallback. Both\n // are wrapped so a runtime exception (malformed config slipped past the\n // resolver, browser API quirks, a bad scope) still produces a usable\n // selector. Without this, an autocapture click handler could throw\n // mid-flow and the customer would silently lose the event — a worse\n // outcome than emitting a less-stable selector.\n try {\n const composed = runOrchestrator(el, config, orchestratorOptions);\n if (composed !== null) {\n return composed;\n }\n return fallbackCssPath(el, config, { scope: options.scope ?? el.ownerDocument ?? document });\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: strategy chain threw — falling back to legacy cssPath: ${message}`);\n return safeLegacyCssPath(el);\n }\n },\n\n getConfig(): Readonly<ResolvedSelectorConfig> {\n return config;\n },\n\n updateConfig(next: ResolvedSelectorConfig): void {\n config = next;\n // Notify subscribers in insertion order. Errors thrown by individual\n // subscribers are isolated so one bad listener can't break the others —\n // the extension's listener is the canonical consumer here and we don't\n // want SDK-side changes to cascade-fail because of an extension bug.\n for (const cb of subscribers) {\n try {\n cb(next);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: onConfigChange subscriber threw — ${message}`);\n }\n }\n },\n\n onConfigChange(cb: (config: ResolvedSelectorConfig) => void): () => void {\n subscribers.add(cb);\n return () => {\n subscribers.delete(cb);\n };\n },\n };\n\n // Local wrapper so a thrown error in the legacy walker (extremely unlikely\n // — Element inputs only — but defensible if a host environment is missing\n // `CSS.escape` or `Node` constants) doesn't escape `generate` and crash the\n // caller. Returns the empty string in that pathological case; consumers\n // already coalesce empty results to a sentinel.\n function safeLegacyCssPath(el: Element): string {\n try {\n return legacyCssPath(el);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: legacyCssPath threw — emitting empty selector: ${message}`);\n return '';\n }\n }\n}\n"]}
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;;AAGH,OAAO,EAAE,eAAe,EAAuB,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAiBtD;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAClC,aAAqC,EACrC,OAAyC;IAAzC,wBAAA,EAAA,YAAyC;IAEzC,IAAI,MAAM,GAA2B,aAAa,CAAC;IACnD,IAAM,WAAW,GAAG,IAAI,GAAG,EAA4C,CAAC;IACxE,IAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAE9B,IAAM,mBAAmB,GAAwB;QAC/C,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,QAAA;KACP,CAAC;IAEF,OAAO;QACL,QAAQ,YAAC,EAAW;;YAClB,gEAAgE;YAChE,uEAAuE;YACvE,qEAAqE;YACrE,mEAAmE;YACnE,wEAAwE;YACxE,6CAA6C;YAC7C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE;gBACnB,OAAO,iBAAiB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;aACtC;YAED,wEAAwE;YACxE,wEAAwE;YACxE,qEAAqE;YACrE,mEAAmE;YACnE,oEAAoE;YACpE,gDAAgD;YAChD,IAAI;gBACF,IAAM,QAAQ,GAAG,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAC;gBAClE,IAAI,QAAQ,KAAK,IAAI,EAAE;oBACrB,OAAO,QAAQ,CAAC;iBACjB;gBACD,OAAO,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAA,MAAA,OAAO,CAAC,KAAK,mCAAI,EAAE,CAAC,aAAa,mCAAI,QAAQ,EAAE,CAAC,CAAC;aAC9F;YAAC,OAAO,CAAC,EAAE;gBACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,mGAAuF,OAAO,CAAE,CAAC,CAAC;gBAC/G,OAAO,iBAAiB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;aACtC;QACH,CAAC;QAED,SAAS;YACP,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,YAAY,YAAC,IAA4B;;YACvC,MAAM,GAAG,IAAI,CAAC;;gBACd,qEAAqE;gBACrE,wEAAwE;gBACxE,uEAAuE;gBACvE,qEAAqE;gBACrE,KAAiB,IAAA,gBAAA,SAAA,WAAW,CAAA,wCAAA,iEAAE;oBAAzB,IAAM,EAAE,wBAAA;oBACX,IAAI;wBACF,EAAE,CAAC,IAAI,CAAC,CAAC;qBACV;oBAAC,OAAO,CAAC,EAAE;wBACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;wBAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,8EAAkE,OAAO,CAAE,CAAC,CAAC;qBAC3F;iBACF;;;;;;;;;QACH,CAAC;QAED,cAAc,YAAC,EAA4C;YACzD,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACpB,OAAO;gBACL,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACzB,CAAC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Engine factory — composes the strategy chain (via `runOrchestrator`) with\n * the safety-net fallback (`fallbackCssPath`) behind a single `SelectorEngine`\n * interface.\n *\n * This is the surface every consumer talks to:\n *\n * - autocapture SDK plugin → instantiates one engine per init() call,\n * wires it into the click handler, and forwards remote-config updates\n * via `updateConfig`.\n * - app.amplitude.com tagging UI → fetches the customer's remote config\n * out-of-band and stands up a transient engine to compute selectors for\n * elements the user clicks in the iframe.\n * - Chrome extension visual tagger → reads the customer's already-live\n * engine off `window.amplitude.elementSelector` (when present) and\n * subscribes to `onConfigChange` so the extension's preview stays in\n * sync with the customer's runtime config.\n *\n * The factory deliberately takes a pre-resolved `ResolvedSelectorConfig` rather\n * than the raw remote payload — config resolution is a separate concern handled\n * by `resolveSelectorConfig`, and keeping it out of the engine constructor lets\n * dashboard / extension consumers stand up an engine from a static snapshot\n * without re-running the full resolver.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ElementSelectorLogger, ResolvedSelectorConfig, SelectorEngine } from './types';\nimport { runOrchestrator, OrchestratorOptions } from './orchestrator';\nimport { fallbackCssPath } from './fallback-css-path';\nimport { safeLegacyCssPath } from './legacy-css-path';\n\nexport interface CreateSelectorEngineOptions {\n /** Optional document or shadow root for uniqueness checks. Defaults per-call to the target's owner document. */\n scope?: ParentNode;\n /** Optional override of the strategy chain. Primarily for testing / dashboard ad-hoc runs. */\n strategies?: OrchestratorOptions['strategies'];\n /**\n * Optional logger threaded through the orchestrator and the subscriber-fan-out\n * inside `updateConfig`. When provided, the engine surfaces malformed\n * selectors (`debug`) and listener exceptions (`warn`); when absent it stays\n * silent — preserving the legacy \"fire-and-forget\" semantics existing\n * consumers may rely on.\n */\n logger?: ElementSelectorLogger;\n}\n\n/**\n * Build a `SelectorEngine` bound to the supplied config.\n *\n * The returned engine is independent — calling the factory twice yields two\n * engines with separate config state and separate subscriber lists. This is the\n * shape the autocapture plugin wants (one engine per SDK instance) and the\n * shape the Chrome extension consumes off the page (the extension reads, never\n * writes).\n */\nexport function createSelectorEngine(\n initialConfig: ResolvedSelectorConfig,\n options: CreateSelectorEngineOptions = {},\n): SelectorEngine {\n let config: ResolvedSelectorConfig = initialConfig;\n const subscribers = new Set<(config: ResolvedSelectorConfig) => void>();\n const logger = options.logger;\n\n const orchestratorOptions: OrchestratorOptions = {\n strategies: options.strategies,\n scope: options.scope,\n logger,\n };\n\n return {\n generate(el: Element): string {\n // Kill switch: when the customer's remote config has the engine\n // disabled (the default for orgs that haven't opted in), emit the same\n // Chromium-derived selector autocapture has shipped since before the\n // strategy chain existed. This is what makes \"flip enabled back to\n // false\" a one-config-fetch revert for both the SDK and the dashboard —\n // they both go through this entry point now.\n if (!config.enabled) {\n return safeLegacyCssPath(el, logger);\n }\n\n // Engine path: try the strategy chain, then the hardened fallback. Both\n // are wrapped so a runtime exception (malformed config slipped past the\n // resolver, browser API quirks, a bad scope) still produces a usable\n // selector. Without this, an autocapture click handler could throw\n // mid-flow and the customer would silently lose the event — a worse\n // outcome than emitting a less-stable selector.\n try {\n const composed = runOrchestrator(el, config, orchestratorOptions);\n if (composed !== null) {\n return composed;\n }\n return fallbackCssPath(el, config, { scope: options.scope ?? el.ownerDocument ?? document });\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: strategy chain threw — falling back to legacy cssPath: ${message}`);\n return safeLegacyCssPath(el, logger);\n }\n },\n\n getConfig(): Readonly<ResolvedSelectorConfig> {\n return config;\n },\n\n updateConfig(next: ResolvedSelectorConfig): void {\n config = next;\n // Notify subscribers in insertion order. Errors thrown by individual\n // subscribers are isolated so one bad listener can't break the others —\n // the extension's listener is the canonical consumer here and we don't\n // want SDK-side changes to cascade-fail because of an extension bug.\n for (const cb of subscribers) {\n try {\n cb(next);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: onConfigChange subscriber threw — ${message}`);\n }\n }\n },\n\n onConfigChange(cb: (config: ResolvedSelectorConfig) => void): () => void {\n subscribers.add(cb);\n return () => {\n subscribers.delete(cb);\n };\n },\n };\n}\n"]}

@@ -1,1 +0,1 @@

{"version":3,"file":"generate-selector.d.ts","sourceRoot":"","sources":["../../src/generate-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGjE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gBAAgB,OACvB,OAAO,UACH,cAAc,GAAG,IAAI,UACrB,sBAAsB,KAC7B,MAiBF,CAAC"}
{"version":3,"file":"generate-selector.d.ts","sourceRoot":"","sources":["../../src/generate-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGjE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,gBAAgB,OACvB,OAAO,UACH,cAAc,GAAG,IAAI,UACrB,sBAAsB,KAC7B,MAkBF,CAAC"}

@@ -31,3 +31,3 @@ /**

*/
import { legacyCssPath } from './legacy-css-path';
import { safeLegacyCssPath } from './legacy-css-path';
/**

@@ -62,13 +62,13 @@ * Generate a selector for `el`, transparently routing to the legacy

// straight to the legacy walker — the same selector the SDK would be
// emitting if `config.enabled === false`. `config` is accepted for symmetry
// with the engine signature and so future versions can extend behavior
// without a breaking change.
// emitting if `config.enabled === false`.
//
// `config` is intentionally inert here: call sites with `engine: null`
// (boot-time Chrome extension, inert dashboard context) have no strategy
// engine to run. Passing `enabled: true` cannot take effect without a live
// engine — legacyCssPath is the only output available. `config` is accepted
// for signature symmetry with the engine-present branch and so future
// versions can extend behavior without a breaking change.
void config;
try {
return legacyCssPath(el);
}
catch (_a) {
return '';
}
return safeLegacyCssPath(el);
};
//# sourceMappingURL=generate-selector.js.map

@@ -1,1 +0,1 @@

{"version":3,"file":"generate-selector.js","sourceRoot":"","sources":["../../src/generate-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,IAAM,gBAAgB,GAAG,UAC9B,EAAW,EACX,MAA6B,EAC7B,MAA8B;IAE9B,IAAI,MAAM,EAAE;QACV,mEAAmE;QACnE,8DAA8D;QAC9D,OAAO,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;KAC5B;IACD,0EAA0E;IAC1E,qEAAqE;IACrE,4EAA4E;IAC5E,uEAAuE;IACvE,6BAA6B;IAC7B,KAAK,MAAM,CAAC;IACZ,IAAI;QACF,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC;KAC1B;IAAC,WAAM;QACN,OAAO,EAAE,CAAC;KACX;AACH,CAAC,CAAC","sourcesContent":["/**\n * Top-level chokepoint for \"generate a selector for this element.\"\n *\n * Both the SDK plugin (`plugin-autocapture-browser`) and the dashboard\n * (`session-replay-ui`) used to maintain their own copy of this routing logic:\n *\n * if (config.enabled && engine) return engine.generate(el);\n * else return cssPath(el, false);\n *\n * Two consumers, two routers, two locally-bundled copies of `cssPath`. As soon\n * as we considered a third consumer (Chrome extension visual tagger, future\n * tagging surfaces) the duplication became untenable — bug fixes had to land\n * three places and the kill-switch semantics could drift between them.\n *\n * Moving the kill switch inside `engine.generate` covered the \"engine present\"\n * case. This helper covers the remaining \"engine is null\" case that some\n * consumers genuinely have:\n *\n * - dashboard's `useElementSelectorEngineContext()` returns\n * `{ engine: null }` when no provider is mounted (tests, content-renderer\n * paths, future call sites outside the canonical mount tree).\n * - Chrome extension boots before the customer's SDK has stood up an engine\n * on the page; reads `null` from `window.amplitude.elementSelector` until\n * the SDK init finishes.\n *\n * In those cases, callers want the legacy `cssPath` selector — same shape the\n * SDK is emitting in production for orgs without the engine. This helper makes\n * that explicit and centralizes the rule so future consumers don't reinvent\n * it.\n */\n\nimport { ResolvedSelectorConfig, SelectorEngine } from './types';\nimport { legacyCssPath } from './legacy-css-path';\n\n/**\n * Generate a selector for `el`, transparently routing to the legacy\n * `cssPath` walker when no engine is available.\n *\n * Why this signature (engine + config separately, rather than just engine):\n * the dashboard's inert context has `engine: null` but still carries a\n * resolved config. Both `null`-engine and engine-with-`enabled: false`\n * collapse to \"legacy `cssPath`\", and we want the same one-line call site\n * to handle both. When the engine *is* present, this just delegates — the\n * engine's own kill switch handles `enabled === false`.\n *\n * Never throws. Returns the empty string in the pathological case where the\n * legacy walker itself can't produce one (non-element nodes; environments\n * missing `CSS.escape`).\n *\n * @param el DOM element to identify.\n * @param engine Live `SelectorEngine`, or `null` when no engine is mounted.\n * @param config Resolved selector config. Only consulted in the null-engine\n * branch; when `engine` is present, the engine consults its\n * own internal config.\n */\nexport const generateSelector = (\n el: Element,\n engine: SelectorEngine | null,\n config: ResolvedSelectorConfig,\n): string => {\n if (engine) {\n // The engine has its own kill switch (`config.enabled === false` →\n // legacy cssPath) and its own try/catch safety net. Trust it.\n return engine.generate(el);\n }\n // Engine-null branch. We don't have a live engine to consult, so we route\n // straight to the legacy walker — the same selector the SDK would be\n // emitting if `config.enabled === false`. `config` is accepted for symmetry\n // with the engine signature and so future versions can extend behavior\n // without a breaking change.\n void config;\n try {\n return legacyCssPath(el);\n } catch {\n return '';\n }\n};\n"]}
{"version":3,"file":"generate-selector.js","sourceRoot":"","sources":["../../src/generate-selector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,IAAM,gBAAgB,GAAG,UAC9B,EAAW,EACX,MAA6B,EAC7B,MAA8B;IAE9B,IAAI,MAAM,EAAE;QACV,mEAAmE;QACnE,8DAA8D;QAC9D,OAAO,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;KAC5B;IACD,0EAA0E;IAC1E,qEAAqE;IACrE,0CAA0C;IAC1C,EAAE;IACF,uEAAuE;IACvE,yEAAyE;IACzE,2EAA2E;IAC3E,4EAA4E;IAC5E,sEAAsE;IACtE,0DAA0D;IAC1D,KAAK,MAAM,CAAC;IACZ,OAAO,iBAAiB,CAAC,EAAE,CAAC,CAAC;AAC/B,CAAC,CAAC","sourcesContent":["/**\n * Top-level chokepoint for \"generate a selector for this element.\"\n *\n * Both the SDK plugin (`plugin-autocapture-browser`) and the dashboard\n * (`session-replay-ui`) used to maintain their own copy of this routing logic:\n *\n * if (config.enabled && engine) return engine.generate(el);\n * else return cssPath(el, false);\n *\n * Two consumers, two routers, two locally-bundled copies of `cssPath`. As soon\n * as we considered a third consumer (Chrome extension visual tagger, future\n * tagging surfaces) the duplication became untenable — bug fixes had to land\n * three places and the kill-switch semantics could drift between them.\n *\n * Moving the kill switch inside `engine.generate` covered the \"engine present\"\n * case. This helper covers the remaining \"engine is null\" case that some\n * consumers genuinely have:\n *\n * - dashboard's `useElementSelectorEngineContext()` returns\n * `{ engine: null }` when no provider is mounted (tests, content-renderer\n * paths, future call sites outside the canonical mount tree).\n * - Chrome extension boots before the customer's SDK has stood up an engine\n * on the page; reads `null` from `window.amplitude.elementSelector` until\n * the SDK init finishes.\n *\n * In those cases, callers want the legacy `cssPath` selector — same shape the\n * SDK is emitting in production for orgs without the engine. This helper makes\n * that explicit and centralizes the rule so future consumers don't reinvent\n * it.\n */\n\nimport { ResolvedSelectorConfig, SelectorEngine } from './types';\nimport { safeLegacyCssPath } from './legacy-css-path';\n\n/**\n * Generate a selector for `el`, transparently routing to the legacy\n * `cssPath` walker when no engine is available.\n *\n * Why this signature (engine + config separately, rather than just engine):\n * the dashboard's inert context has `engine: null` but still carries a\n * resolved config. Both `null`-engine and engine-with-`enabled: false`\n * collapse to \"legacy `cssPath`\", and we want the same one-line call site\n * to handle both. When the engine *is* present, this just delegates — the\n * engine's own kill switch handles `enabled === false`.\n *\n * Never throws. Returns the empty string in the pathological case where the\n * legacy walker itself can't produce one (non-element nodes; environments\n * missing `CSS.escape`).\n *\n * @param el DOM element to identify.\n * @param engine Live `SelectorEngine`, or `null` when no engine is mounted.\n * @param config Resolved selector config. Only consulted in the null-engine\n * branch; when `engine` is present, the engine consults its\n * own internal config.\n */\nexport const generateSelector = (\n el: Element,\n engine: SelectorEngine | null,\n config: ResolvedSelectorConfig,\n): string => {\n if (engine) {\n // The engine has its own kill switch (`config.enabled === false` →\n // legacy cssPath) and its own try/catch safety net. Trust it.\n return engine.generate(el);\n }\n // Engine-null branch. We don't have a live engine to consult, so we route\n // straight to the legacy walker — the same selector the SDK would be\n // emitting if `config.enabled === false`.\n //\n // `config` is intentionally inert here: call sites with `engine: null`\n // (boot-time Chrome extension, inert dashboard context) have no strategy\n // engine to run. Passing `enabled: true` cannot take effect without a live\n // engine — legacyCssPath is the only output available. `config` is accepted\n // for signature symmetry with the engine-present branch and so future\n // versions can extend behavior without a breaking change.\n void config;\n return safeLegacyCssPath(el);\n};\n"]}

@@ -72,2 +72,3 @@ /**

*/
import { ElementSelectorLogger } from './types';
/**

@@ -85,2 +86,10 @@ * Produce a CSS selector string for `node` using the legacy positional walker.

export declare const legacyCssPath: (node: Element, optimized?: boolean) => string;
/**
* Invoke `legacyCssPath` without letting a throw escape to the caller.
*
* Shared by `engine.generate` (kill switch + strategy-chain safety net) and
* `generateSelector` (null-engine branch) so the swallow-warn-fallback
* guarantee lives in one place.
*/
export declare function safeLegacyCssPath(el: Element, logger?: ElementSelectorLogger): string;
//# sourceMappingURL=legacy-css-path.d.ts.map

@@ -1,1 +0,1 @@

{"version":3,"file":"legacy-css-path.d.ts","sourceRoot":"","sources":["../../src/legacy-css-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAgBH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,SAAU,OAAO,cAAc,OAAO,KAAG,MAuBlE,CAAC"}
{"version":3,"file":"legacy-css-path.d.ts","sourceRoot":"","sources":["../../src/legacy-css-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAOH,OAAO,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAUhD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,SAAU,OAAO,cAAc,OAAO,KAAG,MAuBlE,CAAC;AAmHF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAQrF"}

@@ -73,6 +73,2 @@ /**

import { __values } from "tslib";
/* istanbul ignore file -- the algorithm is locked-down behavior inherited from
Chromium and validated by the SDK's own end-to-end suite. Re-running line
coverage here only obscures the parts of the package that *do* have
meaningful coverage. */
import { escapeCssIdentifier } from './helpers/escape-css-identifier';

@@ -227,2 +223,19 @@ var Step = /** @class */ (function () {

var idSelector = function (id) { return '#' + escapeCssIdentifier(id); };
/**
* Invoke `legacyCssPath` without letting a throw escape to the caller.
*
* Shared by `engine.generate` (kill switch + strategy-chain safety net) and
* `generateSelector` (null-engine branch) so the swallow-warn-fallback
* guarantee lives in one place.
*/
export function safeLegacyCssPath(el, logger) {
try {
return legacyCssPath(el);
}
catch (e) {
var message = e instanceof Error ? e.message : String(e);
logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: legacyCssPath threw \u2014 emitting empty selector: ".concat(message));
return '';
}
}
//# sourceMappingURL=legacy-css-path.js.map

@@ -1,1 +0,1 @@

{"version":3,"file":"legacy-css-path.js","sourceRoot":"","sources":["../../src/legacy-css-path.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Canonical legacy CSS-path walker.\n *\n * This is the Chromium-DevTools-derived `cssPath` algorithm that was the only\n * selector source autocapture used before the strategy engine existed. It still\n * matters in two situations:\n *\n * 1. The kill switch: when `ResolvedSelectorConfig.enabled === false`, the\n * engine routes here so a customer who flips the engine off mid-session\n * keeps emitting the same selectors their downstream zones and cohorts\n * were built against.\n * 2. The safety net: when the strategy chain or its internal walker throws\n * anything unexpected, the engine catches and falls back here. The\n * autocapture click handler must always produce *some* selector — a\n * runtime exception in selector generation is a worse outcome than a\n * slightly less stable selector.\n *\n * Until this file existed, the algorithm was duplicated in two places kept\n * \"in sync by comment\":\n *\n * - `packages/plugin-autocapture-browser/src/libs/element-path.ts`\n * - `javascript/packages/session-replay-ui/src/utils/element-path.ts`\n *\n * Both will eventually re-export from here so divergence becomes impossible.\n *\n * Behavior is byte-identical to those two copies — selectors emitted by this\n * function for a given element MUST match what the SDK has been emitting in\n * production. Do not \"fix\" the selector format without an explicit version\n * bump and a coordinated dashboard rollout.\n *\n * **Why this lives in the package, not as an optional consumer concern:**\n *\n * Every consumer that uses `engine.generate(el)` benefits from the kill\n * switch and the safety net automatically. Without this, each new consumer\n * (Chrome extension visual tagger, future tagging surfaces) has to\n * reimplement the same router and the same legacy walker. We learned that\n * the hard way — by writing the same `if (config.enabled && engine) ...\n * else cssPath()` block in two repositories with two copies of cssPath.\n *\n * Code is adapted from The Chromium Authors.\n * Source: https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/panels/elements/DOMPath.ts#L14\n * License: BSD-style license\n *\n * Copyright 2014 The Chromium Authors\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are\n * met:\n *\n * * Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * * Redistributions in binary form must reproduce the above\n * copyright notice, this list of conditions and the following\n * disclaimer in the documentation and/or other materials provided\n * with the distribution.\n * * Neither the name of Google Inc. nor the names of its\n * contributors may be used to endorse or promote products derived\n * from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n * \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n\n/* istanbul ignore file -- the algorithm is locked-down behavior inherited from\n Chromium and validated by the SDK's own end-to-end suite. Re-running line\n coverage here only obscures the parts of the package that *do* have\n meaningful coverage. */\n\nimport { escapeCssIdentifier } from './helpers/escape-css-identifier';\n\nclass Step {\n constructor(public value: string, public optimized: boolean) {}\n toString(): string {\n return this.value;\n }\n}\n\n/**\n * Produce a CSS selector string for `node` using the legacy positional walker.\n *\n * Returns the empty string for non-element nodes — same shape every existing\n * caller depends on. Never throws for well-formed Element inputs.\n *\n * @param node Element to identify.\n * @param optimized When `true`, short-circuits at the nearest unique-ish step\n * (id or `<body>` / `<head>` / `<html>`). Default `false`,\n * which matches what autocapture has always passed.\n */\nexport const legacyCssPath = (node: Element, optimized?: boolean): string => {\n // `node` is already an Element; this check is defensive.\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const steps: Step[] = [];\n let contextNode: Element | null = node;\n\n while (contextNode) {\n const step = cssPathStep(contextNode, Boolean(optimized), contextNode === node);\n if (!step) {\n break;\n }\n steps.push(step);\n if (step.optimized) {\n break;\n }\n contextNode = contextNode.parentElement;\n }\n\n steps.reverse();\n return steps.join(' > ');\n};\n\nconst cssPathStep = (node: Element, optimized: boolean, isTargetNode: boolean): Step | null => {\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return null;\n }\n\n const id = node.getAttribute('id');\n if (optimized) {\n if (id) {\n return new Step(idSelector(id), true);\n }\n\n const nodeNameLower = node.tagName.toLowerCase();\n if (nodeNameLower === 'body' || nodeNameLower === 'head' || nodeNameLower === 'html') {\n return new Step(nodeNameLower, true);\n }\n }\n\n const nodeName = node.tagName.toLowerCase();\n\n if (id) {\n return new Step(nodeName + idSelector(id), true);\n }\n\n const parent = node.parentNode;\n if (!parent || parent.nodeType === Node.DOCUMENT_NODE) {\n return new Step(nodeName, true);\n }\n\n const prefixedOwnClassNamesArray = prefixedElementClassNames(node);\n let needsClassNames = false;\n let needsNthChild = false;\n let ownIndex = -1;\n let elementIndex = -1;\n\n const siblings: HTMLCollectionOf<Element> = (parent as Element | Document).children;\n\n for (let i = 0; siblings && (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {\n const sibling = siblings[i];\n if (sibling.nodeType !== Node.ELEMENT_NODE) {\n continue;\n }\n\n elementIndex += 1;\n if (sibling === node) {\n ownIndex = elementIndex;\n continue;\n }\n if (needsNthChild) {\n continue;\n }\n\n if (sibling.tagName.toLowerCase() !== nodeName) {\n continue;\n }\n\n needsClassNames = true;\n const ownClassNames = new Set<string>(prefixedOwnClassNamesArray);\n if (!ownClassNames.size) {\n needsNthChild = true;\n continue;\n }\n\n const siblingClassNamesArray = prefixedElementClassNames(sibling);\n for (let j = 0; j < siblingClassNamesArray.length; ++j) {\n const siblingClass = siblingClassNamesArray[j];\n if (!ownClassNames.has(siblingClass)) {\n continue;\n }\n ownClassNames.delete(siblingClass);\n if (!ownClassNames.size) {\n needsNthChild = true;\n break;\n }\n }\n }\n\n let result = nodeName;\n if (\n isTargetNode &&\n nodeName.toLowerCase() === 'input' &&\n node.getAttribute('type') &&\n !node.getAttribute('id') &&\n !node.getAttribute('class')\n ) {\n result += '[type=' + escapeCssIdentifier(node.getAttribute('type') || '') + ']';\n }\n if (needsNthChild) {\n result += ':nth-child(' + String(ownIndex + 1) + ')';\n } else if (needsClassNames) {\n for (const prefixedName of prefixedOwnClassNamesArray) {\n result += '.' + escapeCssIdentifier(prefixedName.slice(1));\n }\n }\n\n return new Step(result, false);\n};\n\nconst prefixedElementClassNames = (el: Element): string[] => {\n const classAttribute = el.getAttribute('class');\n if (!classAttribute) {\n return [];\n }\n return classAttribute\n .split(/\\s+/g)\n .filter(Boolean)\n .map((name) => {\n // The prefix is required to store \"__proto__\" in a object-based map.\n return '$' + name;\n });\n};\n\nconst idSelector = (id: string): string => '#' + escapeCssIdentifier(id);\n"]}
{"version":3,"file":"legacy-css-path.js","sourceRoot":"","sources":["../../src/legacy-css-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;;AAQH,OAAO,EAAE,mBAAmB,EAAE,MAAM,iCAAiC,CAAC;AAEtE;IACE,cAAmB,KAAa,EAAS,SAAkB;QAAxC,UAAK,GAAL,KAAK,CAAQ;QAAS,cAAS,GAAT,SAAS,CAAS;IAAG,CAAC;IAC/D,uBAAQ,GAAR;QACE,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IACH,WAAC;AAAD,CAAC,AALD,IAKC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,IAAM,aAAa,GAAG,UAAC,IAAa,EAAE,SAAmB;IAC9D,yDAAyD;IACzD,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,YAAY,EAAE;QACvC,OAAO,EAAE,CAAC;KACX;IAED,IAAM,KAAK,GAAW,EAAE,CAAC;IACzB,IAAI,WAAW,GAAmB,IAAI,CAAC;IAEvC,OAAO,WAAW,EAAE;QAClB,IAAM,IAAI,GAAG,WAAW,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE,WAAW,KAAK,IAAI,CAAC,CAAC;QAChF,IAAI,CAAC,IAAI,EAAE;YACT,MAAM;SACP;QACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjB,IAAI,IAAI,CAAC,SAAS,EAAE;YAClB,MAAM;SACP;QACD,WAAW,GAAG,WAAW,CAAC,aAAa,CAAC;KACzC;IAED,KAAK,CAAC,OAAO,EAAE,CAAC;IAChB,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC,CAAC;AAEF,IAAM,WAAW,GAAG,UAAC,IAAa,EAAE,SAAkB,EAAE,YAAqB;;IAC3E,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,YAAY,EAAE;QACvC,OAAO,IAAI,CAAC;KACb;IAED,IAAM,EAAE,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,SAAS,EAAE;QACb,IAAI,EAAE,EAAE;YACN,OAAO,IAAI,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;SACvC;QAED,IAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;QACjD,IAAI,aAAa,KAAK,MAAM,IAAI,aAAa,KAAK,MAAM,IAAI,aAAa,KAAK,MAAM,EAAE;YACpF,OAAO,IAAI,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;SACtC;KACF;IAED,IAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IAE5C,IAAI,EAAE,EAAE;QACN,OAAO,IAAI,IAAI,CAAC,QAAQ,GAAG,UAAU,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;KAClD;IAED,IAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;IAC/B,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC,aAAa,EAAE;QACrD,OAAO,IAAI,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;KACjC;IAED,IAAM,0BAA0B,GAAG,yBAAyB,CAAC,IAAI,CAAC,CAAC;IACnE,IAAI,eAAe,GAAG,KAAK,CAAC;IAC5B,IAAI,aAAa,GAAG,KAAK,CAAC;IAC1B,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC;IAClB,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC;IAEtB,IAAM,QAAQ,GAA+B,MAA6B,CAAC,QAAQ,CAAC;IAEpF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,QAAQ,IAAI,CAAC,QAAQ,KAAK,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE;QAC3F,IAAM,OAAO,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC5B,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,YAAY,EAAE;YAC1C,SAAS;SACV;QAED,YAAY,IAAI,CAAC,CAAC;QAClB,IAAI,OAAO,KAAK,IAAI,EAAE;YACpB,QAAQ,GAAG,YAAY,CAAC;YACxB,SAAS;SACV;QACD,IAAI,aAAa,EAAE;YACjB,SAAS;SACV;QAED,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,QAAQ,EAAE;YAC9C,SAAS;SACV;QAED,eAAe,GAAG,IAAI,CAAC;QACvB,IAAM,aAAa,GAAG,IAAI,GAAG,CAAS,0BAA0B,CAAC,CAAC;QAClE,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE;YACvB,aAAa,GAAG,IAAI,CAAC;YACrB,SAAS;SACV;QAED,IAAM,sBAAsB,GAAG,yBAAyB,CAAC,OAAO,CAAC,CAAC;QAClE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,sBAAsB,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE;YACtD,IAAM,YAAY,GAAG,sBAAsB,CAAC,CAAC,CAAC,CAAC;YAC/C,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE;gBACpC,SAAS;aACV;YACD,aAAa,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;YACnC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE;gBACvB,aAAa,GAAG,IAAI,CAAC;gBACrB,MAAM;aACP;SACF;KACF;IAED,IAAI,MAAM,GAAG,QAAQ,CAAC;IACtB,IACE,YAAY;QACZ,QAAQ,CAAC,WAAW,EAAE,KAAK,OAAO;QAClC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;QACzB,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC;QACxB,CAAC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAC3B;QACA,MAAM,IAAI,QAAQ,GAAG,mBAAmB,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,GAAG,GAAG,CAAC;KACjF;IACD,IAAI,aAAa,EAAE;QACjB,MAAM,IAAI,aAAa,GAAG,MAAM,CAAC,QAAQ,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC;KACtD;SAAM,IAAI,eAAe,EAAE;;YAC1B,KAA2B,IAAA,+BAAA,SAAA,0BAA0B,CAAA,sEAAA,8GAAE;gBAAlD,IAAM,YAAY,uCAAA;gBACrB,MAAM,IAAI,GAAG,GAAG,mBAAmB,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;aAC5D;;;;;;;;;KACF;IAED,OAAO,IAAI,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AACjC,CAAC,CAAC;AAEF,IAAM,yBAAyB,GAAG,UAAC,EAAW;IAC5C,IAAM,cAAc,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;IAChD,IAAI,CAAC,cAAc,EAAE;QACnB,OAAO,EAAE,CAAC;KACX;IACD,OAAO,cAAc;SAClB,KAAK,CAAC,MAAM,CAAC;SACb,MAAM,CAAC,OAAO,CAAC;SACf,GAAG,CAAC,UAAC,IAAI;QACR,qEAAqE;QACrE,OAAO,GAAG,GAAG,IAAI,CAAC;IACpB,CAAC,CAAC,CAAC;AACP,CAAC,CAAC;AAEF,IAAM,UAAU,GAAG,UAAC,EAAU,IAAa,OAAA,GAAG,GAAG,mBAAmB,CAAC,EAAE,CAAC,EAA7B,CAA6B,CAAC;AAEzE;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAW,EAAE,MAA8B;IAC3E,IAAI;QACF,OAAO,aAAa,CAAC,EAAE,CAAC,CAAC;KAC1B;IAAC,OAAO,CAAC,EAAE;QACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,2FAA+E,OAAO,CAAE,CAAC,CAAC;QACvG,OAAO,EAAE,CAAC;KACX;AACH,CAAC","sourcesContent":["/**\n * Canonical legacy CSS-path walker.\n *\n * This is the Chromium-DevTools-derived `cssPath` algorithm that was the only\n * selector source autocapture used before the strategy engine existed. It still\n * matters in two situations:\n *\n * 1. The kill switch: when `ResolvedSelectorConfig.enabled === false`, the\n * engine routes here so a customer who flips the engine off mid-session\n * keeps emitting the same selectors their downstream zones and cohorts\n * were built against.\n * 2. The safety net: when the strategy chain or its internal walker throws\n * anything unexpected, the engine catches and falls back here. The\n * autocapture click handler must always produce *some* selector — a\n * runtime exception in selector generation is a worse outcome than a\n * slightly less stable selector.\n *\n * Until this file existed, the algorithm was duplicated in two places kept\n * \"in sync by comment\":\n *\n * - `packages/plugin-autocapture-browser/src/libs/element-path.ts`\n * - `javascript/packages/session-replay-ui/src/utils/element-path.ts`\n *\n * Both will eventually re-export from here so divergence becomes impossible.\n *\n * Behavior is byte-identical to those two copies — selectors emitted by this\n * function for a given element MUST match what the SDK has been emitting in\n * production. Do not \"fix\" the selector format without an explicit version\n * bump and a coordinated dashboard rollout.\n *\n * **Why this lives in the package, not as an optional consumer concern:**\n *\n * Every consumer that uses `engine.generate(el)` benefits from the kill\n * switch and the safety net automatically. Without this, each new consumer\n * (Chrome extension visual tagger, future tagging surfaces) has to\n * reimplement the same router and the same legacy walker. We learned that\n * the hard way — by writing the same `if (config.enabled && engine) ...\n * else cssPath()` block in two repositories with two copies of cssPath.\n *\n * Code is adapted from The Chromium Authors.\n * Source: https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/panels/elements/DOMPath.ts#L14\n * License: BSD-style license\n *\n * Copyright 2014 The Chromium Authors\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions are\n * met:\n *\n * * Redistributions of source code must retain the above copyright\n * notice, this list of conditions and the following disclaimer.\n * * Redistributions in binary form must reproduce the above\n * copyright notice, this list of conditions and the following\n * disclaimer in the documentation and/or other materials provided\n * with the distribution.\n * * Neither the name of Google Inc. nor the names of its\n * contributors may be used to endorse or promote products derived\n * from this software without specific prior written permission.\n *\n * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n * \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\n * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\n * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\n * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\n * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n */\n\n/* istanbul ignore file -- the algorithm is locked-down behavior inherited from\n Chromium and validated by the SDK's own end-to-end suite. Re-running line\n coverage here only obscures the parts of the package that *do* have\n meaningful coverage. */\n\nimport { ElementSelectorLogger } from './types';\nimport { escapeCssIdentifier } from './helpers/escape-css-identifier';\n\nclass Step {\n constructor(public value: string, public optimized: boolean) {}\n toString(): string {\n return this.value;\n }\n}\n\n/**\n * Produce a CSS selector string for `node` using the legacy positional walker.\n *\n * Returns the empty string for non-element nodes — same shape every existing\n * caller depends on. Never throws for well-formed Element inputs.\n *\n * @param node Element to identify.\n * @param optimized When `true`, short-circuits at the nearest unique-ish step\n * (id or `<body>` / `<head>` / `<html>`). Default `false`,\n * which matches what autocapture has always passed.\n */\nexport const legacyCssPath = (node: Element, optimized?: boolean): string => {\n // `node` is already an Element; this check is defensive.\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return '';\n }\n\n const steps: Step[] = [];\n let contextNode: Element | null = node;\n\n while (contextNode) {\n const step = cssPathStep(contextNode, Boolean(optimized), contextNode === node);\n if (!step) {\n break;\n }\n steps.push(step);\n if (step.optimized) {\n break;\n }\n contextNode = contextNode.parentElement;\n }\n\n steps.reverse();\n return steps.join(' > ');\n};\n\nconst cssPathStep = (node: Element, optimized: boolean, isTargetNode: boolean): Step | null => {\n if (node.nodeType !== Node.ELEMENT_NODE) {\n return null;\n }\n\n const id = node.getAttribute('id');\n if (optimized) {\n if (id) {\n return new Step(idSelector(id), true);\n }\n\n const nodeNameLower = node.tagName.toLowerCase();\n if (nodeNameLower === 'body' || nodeNameLower === 'head' || nodeNameLower === 'html') {\n return new Step(nodeNameLower, true);\n }\n }\n\n const nodeName = node.tagName.toLowerCase();\n\n if (id) {\n return new Step(nodeName + idSelector(id), true);\n }\n\n const parent = node.parentNode;\n if (!parent || parent.nodeType === Node.DOCUMENT_NODE) {\n return new Step(nodeName, true);\n }\n\n const prefixedOwnClassNamesArray = prefixedElementClassNames(node);\n let needsClassNames = false;\n let needsNthChild = false;\n let ownIndex = -1;\n let elementIndex = -1;\n\n const siblings: HTMLCollectionOf<Element> = (parent as Element | Document).children;\n\n for (let i = 0; siblings && (ownIndex === -1 || !needsNthChild) && i < siblings.length; ++i) {\n const sibling = siblings[i];\n if (sibling.nodeType !== Node.ELEMENT_NODE) {\n continue;\n }\n\n elementIndex += 1;\n if (sibling === node) {\n ownIndex = elementIndex;\n continue;\n }\n if (needsNthChild) {\n continue;\n }\n\n if (sibling.tagName.toLowerCase() !== nodeName) {\n continue;\n }\n\n needsClassNames = true;\n const ownClassNames = new Set<string>(prefixedOwnClassNamesArray);\n if (!ownClassNames.size) {\n needsNthChild = true;\n continue;\n }\n\n const siblingClassNamesArray = prefixedElementClassNames(sibling);\n for (let j = 0; j < siblingClassNamesArray.length; ++j) {\n const siblingClass = siblingClassNamesArray[j];\n if (!ownClassNames.has(siblingClass)) {\n continue;\n }\n ownClassNames.delete(siblingClass);\n if (!ownClassNames.size) {\n needsNthChild = true;\n break;\n }\n }\n }\n\n let result = nodeName;\n if (\n isTargetNode &&\n nodeName.toLowerCase() === 'input' &&\n node.getAttribute('type') &&\n !node.getAttribute('id') &&\n !node.getAttribute('class')\n ) {\n result += '[type=' + escapeCssIdentifier(node.getAttribute('type') || '') + ']';\n }\n if (needsNthChild) {\n result += ':nth-child(' + String(ownIndex + 1) + ')';\n } else if (needsClassNames) {\n for (const prefixedName of prefixedOwnClassNamesArray) {\n result += '.' + escapeCssIdentifier(prefixedName.slice(1));\n }\n }\n\n return new Step(result, false);\n};\n\nconst prefixedElementClassNames = (el: Element): string[] => {\n const classAttribute = el.getAttribute('class');\n if (!classAttribute) {\n return [];\n }\n return classAttribute\n .split(/\\s+/g)\n .filter(Boolean)\n .map((name) => {\n // The prefix is required to store \"__proto__\" in a object-based map.\n return '$' + name;\n });\n};\n\nconst idSelector = (id: string): string => '#' + escapeCssIdentifier(id);\n\n/**\n * Invoke `legacyCssPath` without letting a throw escape to the caller.\n *\n * Shared by `engine.generate` (kill switch + strategy-chain safety net) and\n * `generateSelector` (null-engine branch) so the swallow-warn-fallback\n * guarantee lives in one place.\n */\nexport function safeLegacyCssPath(el: Element, logger?: ElementSelectorLogger): string {\n try {\n return legacyCssPath(el);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: legacyCssPath threw — emitting empty selector: ${message}`);\n return '';\n }\n}\n"]}
{
"name": "@amplitude/element-selector",
"version": "0.1.1-featelement-selector-v1.0",
"version": "0.2.0-element-path-v1.0",
"description": "Shared element-selector algorithm consumed by autocapture, the Amplitude dashboard, and the Chrome extension visual tagger.",

@@ -26,2 +26,5 @@ "author": "Amplitude Inc",

},
"devDependencies": {
"esbuild": "^0.25.0"
},
"files": [

@@ -43,4 +46,6 @@ "lib"

"test": "jest",
"testbed": "pnpm testbed:build && python3 -m http.server --directory testbed 4567",
"testbed:build": "pnpm build && esbuild testbed/testbed.js --bundle --format=esm --outfile=testbed/build/bundle.js",
"typecheck": "tsc -p ./tsconfig.json"
}
}

@@ -144,2 +144,13 @@ # @amplitude/element-selector

## 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:
```bash
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

@@ -146,0 +157,0 @@