Skip to content

feat(element-selector): add stable selector config hashing - #1841

Draft
jxiwang wants to merge 4 commits into
mainfrom
cursor/element-selector-config-hash-7d25
Draft

feat(element-selector): add stable selector config hashing#1841
jxiwang wants to merge 4 commits into
mainfrom
cursor/element-selector-config-hash-7d25

Conversation

@jxiwang

@jxiwang jxiwang commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a deterministic fingerprint of the resolved selector-algorithm config to @amplitude/element-selector, so the autocapture "zoning" events ([Amplitude] Page Viewed, [Amplitude] Element Clicked, [Amplitude] Rage Click, [Amplitude] Viewport Content Updated) can later record which algorithm configuration was active when they fired.

This PR is purely the element-selector primitives. Plugin integration (wiring the hash onto the actual events) lands separately. The constant and hash function live here because the four events are emitted by three independent plugins in separate packages (autocapture, frustration, page-view-tracking) — none can import from another, but all can depend on @amplitude/element-selector, making it the single source of truth.

What's added

  • canonicalizeSelectorConfig(config) — RFC 8785 (JCS) canonical JSON of a JSON-safe snapshot of ResolvedSelectorConfig, via the canonicalize npm package. Canonicalization decisions:
    • Hashes the fully-resolved config (defaults already applied), so a field omitted from remote config and a field explicitly set to its default produce the same hash.
    • Object keys sorted recursively; numbers normalized; no insignificant whitespace (all via canonicalize).
    • All strings NFC-normalized so equivalent Unicode sequences hash identically.
    • RegExp patterns serialized via toString() (/source/flags), not .source — the default autogeneratedIdPatterns ships a flagged regex (/^[a-f0-9]{16,}$/i), and .source would silently drop the flag. Pattern array order is preserved (it's part of the config).
    • maxAncestorWalkDepth: undefined normalized to null.
  • hashSelectorConfig(config)synchronous SHA-256 (lowercase hex) of the canonical string.
    • Chose a small dependency-free synchronous implementation over crypto.subtle because the latter is async and unavailable in insecure (http) contexts — both fatal for attaching a value on the synchronous tracking path. Output is byte-for-byte identical to Node's crypto/TextEncoder (verified against a 500+ input corpus incl. multi-byte UTF-8, surrogate pairs, and all padding boundaries; lone surrogates map to U+FFFD to match TextEncoder).
  • AMPLITUDE_EVENT_PROP_SELECTOR_ALGO_CONFIG_HASH = '[Amplitude] Selector Algo Config Hash' — centralized here so all three plugins share one property key.
  • Helpers exported for testing/diagnostics: toSelectorConfigSnapshot, sha256Hex, type SelectorConfigSnapshot.

Verification

  • pnpm --filter @amplitude/element-selector test — 278 passed (added 37 tests across sha256 and hash-config).
  • pnpm build, pnpm lint (0 errors), pnpm docs:check — all pass.
  • size-limit: element-selector ESM 3.28 kB / 7 kB cap (canonicalize + SHA-256 added ~0.6 kB gzipped).

Checklist

  • Does your PR title have the correct title format?
  • Does your PR have a breaking change?: No — additive only (new exports, new files, one new dependency).
Open in Web Open in Cursor 

…uting

Extracted from #1832.

- Move canonical legacyCssPath walker into @amplitude/element-selector
- Add generateSelector top-level helper for null-engine call sites
- Route engine.generate through kill-switch and safety-net fallbacks
- Publish package (remove private flag)
- Bump size-limit cap to absorb consolidated walker
Add a deterministic fingerprint of the resolved selector-algorithm config so
the autocapture zoning events (Page Viewed, Element Clicked, Rage Click,
Viewport Content Updated) can record which config was active when they fired.

- canonicalizeSelectorConfig: RFC 8785 (JCS) canonical JSON of a JSON-safe
  snapshot of ResolvedSelectorConfig. Hashes the fully-resolved config so an
  omitted field and an explicit default produce the same hash. NFC-normalizes
  strings; serializes RegExp via toString() so flags are part of the
  fingerprint; normalizes undefined depth to null.
- hashSelectorConfig: synchronous SHA-256 (lowercase hex) of the canonical
  string. Sync + dependency-free so it works in insecure (http) contexts and
  on the synchronous tracking path, unlike crypto.subtle.
- AMPLITUDE_EVENT_PROP_SELECTOR_ALGO_CONFIG_HASH constant, centralized here so
  the three independent emitting plugins can share one property key.

Plugin integration lands separately.
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
packages/analytics-browser/lib/scripts/amplitude-min.js.gz 60.57 KB (0%)
packages/session-replay-browser/lib/scripts/session-replay-browser-min.js.gz 133.19 KB (0%)
packages/unified/lib/scripts/amplitude-min.umd.js.gz 213.16 KB (0%)
@amplitude/element-selector (gzipped esm) 3.21 KB (+20.31% 🔺)

Base automatically changed from cursor/element-selector-extract-8423 to main June 17, 2026 19:59
… merge

The 'Merge main' commit left an extra blank line before the closing brace of
createSelectorEngine, which failed the prettier lint check. Align with main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants