From 9ddd365e98151b1509b043a34245dd9dc207da86 Mon Sep 17 00:00:00 2001 From: Alvin Wang Date: Fri, 22 May 2026 08:59:11 -0700 Subject: [PATCH 1/2] feat(plugin-autocapture-browser): add captureCssClasses option (DMT-525) Adds an `elementInteractions.captureCssClasses` option (default true) that, when set to false, omits the `classes` field from every entry in the captured element-interaction hierarchy. The top-level `[Amplitude] Element Class` property is intentionally not affected. The effective value is read live from the options bag on every capture so that in-place updates delivered via the existing `elementInteractions` remote-config channel (the same pattern used for `pageActions`) are honored on the next capture without SDK reinitialization. The live options bag is also forwarded to the Visual Labeling selector iframe via the messenger handshake so the UI / AI flows stay consistent with what the SDK is capturing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/types/element-interactions.ts | 14 ++ .../src/autocapture-plugin.ts | 7 + .../src/data-extractor.ts | 18 +- .../src/hierarchy.ts | 12 +- .../src/libs/messenger.ts | 25 ++- .../test/data-extractor.test.ts | 162 ++++++++++++++++++ .../default-event-tracking-advanced.test.ts | 50 ++++++ .../test/hierarchy.test.ts | 76 ++++++++ 8 files changed, 358 insertions(+), 6 deletions(-) diff --git a/packages/analytics-core/src/types/element-interactions.ts b/packages/analytics-core/src/types/element-interactions.ts index bfe9c7e65..fef251ae7 100644 --- a/packages/analytics-core/src/types/element-interactions.ts +++ b/packages/analytics-core/src/types/element-interactions.ts @@ -76,6 +76,20 @@ export interface ElementInteractionsOptions { */ dataAttributePrefix?: string; + /** + * Whether CSS class names should be captured in the element hierarchy. + * Default is true. + * + * When set to false, each entry in the captured element-interaction + * hierarchy will omit the `classes` field entirely (the field is not + * present as `null` or an empty array). This only affects the + * hierarchy `classes` field — other captured properties that may + * contain class-like values (e.g., the top-level + * `[Amplitude] Element Class` event property) are not affected by + * this option. + */ + captureCssClasses?: boolean; + /** * Options for integrating visual tagging selector. */ diff --git a/packages/plugin-autocapture-browser/src/autocapture-plugin.ts b/packages/plugin-autocapture-browser/src/autocapture-plugin.ts index e700ffce3..c28026a89 100644 --- a/packages/plugin-autocapture-browser/src/autocapture-plugin.ts +++ b/packages/plugin-autocapture-browser/src/autocapture-plugin.ts @@ -446,6 +446,13 @@ export const autocapturePlugin = ( isElementSelectable: createShouldTrackEvent(options, [...allowlist, ...actionClickAllowlist]), cssSelectorAllowlist: allowlist, actionClickAllowlist, + // Expose the full effective `elementInteractions` options bag + // (live reference; reads through to remote-config-driven + // mutations of `options` in place) to the Visual Labeling + // handshake so the selector iframe / AI flow can branch on + // `captureCssClasses` and any future `elementInteractions` + // capture toggle. + elementInteractionsOptions: options, }); enableBackgroundCapture(messenger); /* istanbul ignore next */ diff --git a/packages/plugin-autocapture-browser/src/data-extractor.ts b/packages/plugin-autocapture-browser/src/data-extractor.ts index 564a03973..52aeaadf0 100644 --- a/packages/plugin-autocapture-browser/src/data-extractor.ts +++ b/packages/plugin-autocapture-browser/src/data-extractor.ts @@ -28,10 +28,18 @@ import { cssPath } from './libs/element-path'; export class DataExtractor { private readonly additionalMaskTextPatterns: RegExp[]; + // Live reference to the autocapture plugin's `ElementInteractionsOptions` + // bag. Read per-capture (not cached at construction time) so that updates + // delivered via the SDK's existing `elementInteractions` remote-config + // delivery — which mutates this bag in place, the same way `pageActions` + // is updated at `autocapture-plugin.ts:249-263` — are honored on the + // next capture without requiring SDK reinitialization. + private readonly options: ElementInteractionsOptions; diagnosticsClient?: IDiagnosticsClient; constructor(options: ElementInteractionsOptions, context?: { diagnosticsClient: IDiagnosticsClient }) { this.diagnosticsClient = context?.diagnosticsClient; + this.options = options; const rawPatterns = options.maskTextRegex ?? []; @@ -88,8 +96,16 @@ export class DataExtractor { } } + // Read the effective `captureCssClasses` value live from the options + // bag on every capture. Default `true` (option `undefined` or `true`) + // preserves the previous wire-format. Setting `false` — whether at + // plugin construction or via a later in-place update to the + // already-merged `elementInteractions` options bag — omits the + // `classes` field from every hierarchy entry on the next capture. + const captureCssClasses = this.options.captureCssClasses !== false; + hierarchy = ancestors.map((el) => - getElementProperties(el, elementToAttributesToMaskMap.get(el) ?? new Set()), + getElementProperties(el, elementToAttributesToMaskMap.get(el) ?? new Set(), captureCssClasses), ); // Search for and mask any sensitive attribute values diff --git a/packages/plugin-autocapture-browser/src/hierarchy.ts b/packages/plugin-autocapture-browser/src/hierarchy.ts index da9f3a69a..368f515b4 100644 --- a/packages/plugin-autocapture-browser/src/hierarchy.ts +++ b/packages/plugin-autocapture-browser/src/hierarchy.ts @@ -44,6 +44,7 @@ export const MAX_HIERARCHY_LENGTH = 1024; export function getElementProperties( element: Element | null, userMaskedAttributeNames: Set, + captureCssClasses = true, ): HierarchyNode | null { if (element === null) { return null; @@ -70,9 +71,14 @@ export function getElementProperties( properties.id = String(id); } - const classes = Array.from(element.classList); - if (classes.length) { - properties.classes = classes; + // When captureCssClasses is false, omit the `classes` field from each + // hierarchy entry entirely (never `null`, never `[]`). Default `true` + // preserves byte-equivalent behavior with previous SDK versions. + if (captureCssClasses) { + const classes = Array.from(element.classList); + if (classes.length) { + properties.classes = classes; + } } const attributes: Record = {}; diff --git a/packages/plugin-autocapture-browser/src/libs/messenger.ts b/packages/plugin-autocapture-browser/src/libs/messenger.ts index 123adccdc..75177960c 100644 --- a/packages/plugin-autocapture-browser/src/libs/messenger.ts +++ b/packages/plugin-autocapture-browser/src/libs/messenger.ts @@ -1,7 +1,7 @@ /* istanbul ignore file */ /* eslint-disable no-restricted-globals */ import { AMPLITUDE_VISUAL_TAGGING_SELECTOR_SCRIPT_URL, AMPLITUDE_VISUAL_TAGGING_HIGHLIGHT_CLASS } from '../constants'; -import type { BaseWindowMessenger } from '@amplitude/analytics-core'; +import type { BaseWindowMessenger, ElementInteractionsOptions } from '@amplitude/analytics-core'; import { ActionType } from '@amplitude/analytics-core'; import { VERSION } from '../version'; import { DataExtractor } from '../data-extractor'; @@ -83,6 +83,19 @@ export function enableVisualTagging( cssSelectorAllowlist?: string[]; actionClickAllowlist?: string[]; dataExtractor: DataExtractor; + /** + * Live reference to the autocapture plugin's effective + * `ElementInteractionsOptions` bag (post-merge of local options + + * remote-config delivery). Because mutations to this bag — the + * channel the SDK's existing `elementInteractions` remote-config + * delivery uses, mirroring the `pageActions` pattern — are visible + * through this same reference, no getter indirection is needed. + * Forwarded to the visual-tagging selector instance so the iframe + * can branch on `captureCssClasses` and any future + * `elementInteractions` capture toggle without re-extending the + * messenger contract. + */ + elementInteractionsOptions?: ElementInteractionsOptions; }, ): void { // Idempotency guard — works across bundle boundaries @@ -92,7 +105,8 @@ export function enableVisualTagging( } branded[VISUAL_TAGGING_BRAND] = true; - const { dataExtractor, isElementSelectable, cssSelectorAllowlist, actionClickAllowlist } = options; + const { dataExtractor, isElementSelectable, cssSelectorAllowlist, actionClickAllowlist, elementInteractionsOptions } = + options; let amplitudeVisualTaggingSelectorInstance: any = null; @@ -131,6 +145,13 @@ export function enableVisualTagging( actionClickAllowlist, extractDataFromDataSource: dataExtractor.extractDataFromDataSource, dataExtractor, + // Live reference to the effective `elementInteractions` + // options bag. The selector iframe reads + // `captureCssClasses` (and any future `elementInteractions` + // capture toggle) directly off this object so the UI / AI + // flows stay consistent with what the SDK is actually + // capturing. + elementInteractionsOptions, diagnostics: { autocapture: { version: VERSION, diff --git a/packages/plugin-autocapture-browser/test/data-extractor.test.ts b/packages/plugin-autocapture-browser/test/data-extractor.test.ts index 9d56d348f..c04473066 100644 --- a/packages/plugin-autocapture-browser/test/data-extractor.test.ts +++ b/packages/plugin-autocapture-browser/test/data-extractor.test.ts @@ -1126,6 +1126,168 @@ describe('data extractor', () => { dataExtractorWithoutDiagnostics.getHierarchy(target); }).not.toThrow(); }); + + describe('captureCssClasses option threading', () => { + const HTML_FIXTURE = ` +
+
+
xxx
+
+
+ `; + + test('default-on (option unset): hierarchy entries with classes are populated', () => { + const extractor = new DataExtractor({}); + document.getElementsByTagName('body')[0].innerHTML = HTML_FIXTURE; + + const hierarchy = extractor.getHierarchy(document.getElementById('inner')); + + // First three entries (inner, parent1, parent2) all have class attributes. + expect(hierarchy[0]).toHaveProperty('classes', ['leaf', 'leaf-two']); + expect(hierarchy[1]).toHaveProperty('classes', ['parent']); + expect(hierarchy[2]).toHaveProperty('classes', ['grandparent', 'gp2']); + }); + + test('default-on (captureCssClasses: true): hierarchy entries with classes are populated', () => { + const extractor = new DataExtractor({ captureCssClasses: true }); + document.getElementsByTagName('body')[0].innerHTML = HTML_FIXTURE; + + const hierarchy = extractor.getHierarchy(document.getElementById('inner')); + + expect(hierarchy[0]).toHaveProperty('classes', ['leaf', 'leaf-two']); + expect(hierarchy[1]).toHaveProperty('classes', ['parent']); + expect(hierarchy[2]).toHaveProperty('classes', ['grandparent', 'gp2']); + }); + + test('default-on path produces byte-equivalent JSON to today', () => { + const extractorDefault = new DataExtractor({}); + const extractorExplicit = new DataExtractor({ captureCssClasses: true }); + document.getElementsByTagName('body')[0].innerHTML = HTML_FIXTURE; + + const inner = document.getElementById('inner'); + const defaultPayload = JSON.stringify(extractorDefault.getHierarchy(inner)); + const explicitPayload = JSON.stringify(extractorExplicit.getHierarchy(inner)); + + expect(defaultPayload).toEqual(explicitPayload); + }); + + test('off path: every hierarchy entry omits the `classes` field entirely', () => { + const extractor = new DataExtractor({ captureCssClasses: false }); + document.getElementsByTagName('body')[0].innerHTML = HTML_FIXTURE; + + const hierarchy = extractor.getHierarchy(document.getElementById('inner')); + + for (const node of hierarchy) { + expect(node ?? {}).not.toHaveProperty('classes'); + } + + // Field must be fully absent — not present as null, not present as []. + const serialized = JSON.stringify(hierarchy); + expect(serialized).not.toContain('"classes"'); + }); + + test('off path: hierarchy preserves non-class properties unchanged', () => { + const extractor = new DataExtractor({ captureCssClasses: false }); + document.getElementsByTagName('body')[0].innerHTML = HTML_FIXTURE; + + const hierarchy = extractor.getHierarchy(document.getElementById('inner')); + + expect(hierarchy[0]).toEqual({ + id: 'inner', + index: 0, + indexOfType: 0, + tag: 'div', + }); + expect(hierarchy[1]).toEqual({ + id: 'parent1', + index: 0, + indexOfType: 0, + tag: 'div', + }); + }); + + test('top-level [Amplitude] Element Class property is still populated when captureCssClasses is false (AC4)', () => { + const extractor = new DataExtractor({ captureCssClasses: false }); + document.getElementsByTagName('body')[0].innerHTML = ` +
+ +
+ `; + + const target = document.getElementById('target') as Element; + const properties = extractor.getEventProperties('click', target, 'data-amp-track-'); + + // Hierarchy must not carry `classes` anywhere. + const hierarchyJson = JSON.stringify(properties[constants.AMPLITUDE_EVENT_PROP_ELEMENT_HIERARCHY]); + expect(hierarchyJson).not.toContain('"classes"'); + + // The explicitly out-of-scope top-level property still carries the class string. + expect(properties[constants.AMPLITUDE_EVENT_PROP_ELEMENT_CLASS]).toBe('cta primary'); + }); + + test('top-level [Amplitude] Element Class property is unchanged when captureCssClasses is true', () => { + const extractor = new DataExtractor({ captureCssClasses: true }); + document.getElementsByTagName('body')[0].innerHTML = ` +
+ +
+ `; + + const target = document.getElementById('target') as Element; + const properties = extractor.getEventProperties('click', target, 'data-amp-track-'); + + expect(properties[constants.AMPLITUDE_EVENT_PROP_ELEMENT_CLASS]).toBe('cta primary'); + }); + + // The effective value must be read live from the options bag on + // every capture, not cached at construction time, so that an + // in-place update to `options.captureCssClasses` — which is how the + // SDK's existing `elementInteractions` remote-config delivery + // surfaces an updated value, mirroring the `pageActions` pattern at + // `autocapture-plugin.ts:249-263` — is honored on the next capture + // without requiring SDK reinitialization. + test('honors in-place options-bag updates on the next capture (default → off)', () => { + const options: { captureCssClasses?: boolean } = {}; + const extractor = new DataExtractor(options); + document.getElementsByTagName('body')[0].innerHTML = HTML_FIXTURE; + + // First pass: default-on, classes captured. + const firstHierarchy = extractor.getHierarchy(document.getElementById('inner')); + expect(firstHierarchy[0]).toHaveProperty('classes', ['leaf', 'leaf-two']); + + // In-place update to the same options object (simulates remote + // config delivering `captureCssClasses: false` into the merged + // `elementInteractions` bag). + options.captureCssClasses = false; + + // Second pass: classes are now fully absent from every entry. + const secondHierarchy = extractor.getHierarchy(document.getElementById('inner')); + for (const node of secondHierarchy) { + expect(node ?? {}).not.toHaveProperty('classes'); + } + expect(JSON.stringify(secondHierarchy)).not.toContain('"classes"'); + }); + + test('honors in-place options-bag updates on the next capture (off → on)', () => { + const options: { captureCssClasses?: boolean } = { captureCssClasses: false }; + const extractor = new DataExtractor(options); + document.getElementsByTagName('body')[0].innerHTML = HTML_FIXTURE; + + // First pass: off, classes absent. + const firstHierarchy = extractor.getHierarchy(document.getElementById('inner')); + expect(JSON.stringify(firstHierarchy)).not.toContain('"classes"'); + + // In-place flip back on (e.g. remote config rolling back the + // toggle for this project). + options.captureCssClasses = true; + + // Second pass: classes are present again on every classed entry. + const secondHierarchy = extractor.getHierarchy(document.getElementById('inner')); + expect(secondHierarchy[0]).toHaveProperty('classes', ['leaf', 'leaf-two']); + expect(secondHierarchy[1]).toHaveProperty('classes', ['parent']); + expect(secondHierarchy[2]).toHaveProperty('classes', ['grandparent', 'gp2']); + }); + }); }); describe('getEventProperties with title masking', () => { diff --git a/packages/plugin-autocapture-browser/test/default-event-tracking-advanced.test.ts b/packages/plugin-autocapture-browser/test/default-event-tracking-advanced.test.ts index 0d77d8a7d..b5327f149 100644 --- a/packages/plugin-autocapture-browser/test/default-event-tracking-advanced.test.ts +++ b/packages/plugin-autocapture-browser/test/default-event-tracking-advanced.test.ts @@ -5,6 +5,7 @@ import { BrowserConfig, EnrichmentPlugin, ILogger, BrowserClient, IDiagnosticsCl import { mockWindowLocationFromURL } from './utils'; import { VERSION } from '../src/version'; import { createMockBrowserClient } from './mock-browser-client'; +import * as messengerLib from '../src/libs/messenger'; const TESTING_DEBOUNCE_TIME = 4; @@ -127,6 +128,55 @@ describe('autoTrackingPlugin', () => { expect((messengerMock as any).setup).toHaveBeenCalledTimes(1); }); + // The autocapture plugin must pass a live reference to the + // effective `elementInteractions` options bag to + // `enableVisualTagging` so the selector iframe / AI flow can read + // `captureCssClasses` (and any future toggle) from it via the + // messenger handshake. + test('passes a live elementInteractionsOptions reference to enableVisualTagging', async () => { + window.opener = true; + const messengerMock = { + setup: jest.fn(), + registerActionHandler: jest.fn(), + }; + jest.spyOn(AnalyticsCore, 'getOrCreateWindowMessenger').mockReturnValue(messengerMock as any); + jest.spyOn(AnalyticsCore, 'enableBackgroundCapture').mockImplementation(jest.fn()); + const enableVisualTaggingSpy = jest.spyOn(messengerLib, 'enableVisualTagging').mockImplementation(jest.fn()); + + plugin = autocapturePlugin({ + visualTaggingOptions: { enabled: true }, + captureCssClasses: true, + }); + const loggerProvider: Partial = { + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + const config: Partial = { + defaultTracking: false, + loggerProvider: loggerProvider as ILogger, + }; + const amplitude: Partial = {}; + await plugin?.setup?.(config as BrowserConfig, amplitude as BrowserClient); + + expect(enableVisualTaggingSpy).toHaveBeenCalledTimes(1); + const callArgs = enableVisualTaggingSpy.mock.calls[0][1] as { + elementInteractionsOptions?: AnalyticsCore.ElementInteractionsOptions; + }; + // The passed value is the live options bag itself (not a getter). + expect(typeof callArgs.elementInteractionsOptions).toBe('object'); + expect(callArgs.elementInteractionsOptions).not.toBeNull(); + expect(callArgs.elementInteractionsOptions!.captureCssClasses).toBe(true); + + // Mutating the plugin's options bag in place (the same mutation + // pattern the SDK's existing remote-config delivery uses for + // `elementInteractions`) surfaces through the same reference — + // this is how the iframe will pick up remote-config flips + // without having to re-handshake. + callArgs.elementInteractionsOptions!.captureCssClasses = false; + expect(callArgs.elementInteractionsOptions!.captureCssClasses).toBe(false); + }); + test('should use custom exposureDuration from deprecated flat option', async () => { const customDuration = 500; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/plugin-autocapture-browser/test/hierarchy.test.ts b/packages/plugin-autocapture-browser/test/hierarchy.test.ts index 42e4432e2..ec03ec470 100644 --- a/packages/plugin-autocapture-browser/test/hierarchy.test.ts +++ b/packages/plugin-autocapture-browser/test/hierarchy.test.ts @@ -90,6 +90,82 @@ describe('autocapture-plugin hierarchy', () => { classes: ['class1', 'class2'], }); }); + + describe('captureCssClasses option', () => { + test('should include classes when captureCssClasses defaults (omitted argument)', () => { + document.getElementsByTagName('body')[0].innerHTML = ` +
+
xxx
+
+ `; + + const inner = document.getElementById('inner'); + // No third argument — must behave byte-equivalent to today. + expect(HierarchyUtil.getElementProperties(inner, new Set())).toEqual({ + id: 'inner', + index: 0, + indexOfType: 0, + tag: 'div', + classes: ['class1', 'class2'], + }); + }); + + test('should include classes when captureCssClasses is explicitly true', () => { + document.getElementsByTagName('body')[0].innerHTML = ` +
+
xxx
+
+ `; + + const inner = document.getElementById('inner'); + expect(HierarchyUtil.getElementProperties(inner, new Set(), true)).toEqual({ + id: 'inner', + index: 0, + indexOfType: 0, + tag: 'div', + classes: ['class1', 'class2'], + }); + }); + + test('should omit `classes` field entirely when captureCssClasses is false', () => { + document.getElementsByTagName('body')[0].innerHTML = ` +
+
xxx
+
+ `; + + const inner = document.getElementById('inner'); + const result = HierarchyUtil.getElementProperties(inner, new Set(), false); + + // Field is fully absent — not null, not []. + expect(result).not.toBeNull(); + expect(result).not.toHaveProperty('classes'); + // Other fields are unchanged. + expect(result).toEqual({ + id: 'inner', + index: 0, + indexOfType: 0, + tag: 'div', + }); + }); + + test('should omit `classes` when captureCssClasses is false even on highly sensitive input types', () => { + document.getElementsByTagName('body')[0].innerHTML = ` + + `; + + const target = document.getElementById('target'); + const result = HierarchyUtil.getElementProperties(target, new Set(), false); + + expect(result).not.toHaveProperty('classes'); + expect(result).toEqual({ + id: 'target', + index: 0, + indexOfType: 0, + tag: 'input', + }); + }); + }); }); test('should not fail when parent element is null', () => { From 4639d399d6ef306e1187729d08d2998ef06c45a5 Mon Sep 17 00:00:00 2001 From: Alvin Wang Date: Fri, 22 May 2026 09:05:54 -0700 Subject: [PATCH 2/2] chore(plugin-autocapture-browser): drop verbose comments on captureCssClasses Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/autocapture-plugin.ts | 6 --- .../src/data-extractor.ts | 12 ----- .../src/hierarchy.ts | 3 -- .../src/libs/messenger.ts | 18 ------- .../test/data-extractor.test.ts | 49 ------------------- .../default-event-tracking-advanced.test.ts | 20 +------- .../test/hierarchy.test.ts | 1 - 7 files changed, 2 insertions(+), 107 deletions(-) diff --git a/packages/plugin-autocapture-browser/src/autocapture-plugin.ts b/packages/plugin-autocapture-browser/src/autocapture-plugin.ts index c28026a89..2c773f46e 100644 --- a/packages/plugin-autocapture-browser/src/autocapture-plugin.ts +++ b/packages/plugin-autocapture-browser/src/autocapture-plugin.ts @@ -446,12 +446,6 @@ export const autocapturePlugin = ( isElementSelectable: createShouldTrackEvent(options, [...allowlist, ...actionClickAllowlist]), cssSelectorAllowlist: allowlist, actionClickAllowlist, - // Expose the full effective `elementInteractions` options bag - // (live reference; reads through to remote-config-driven - // mutations of `options` in place) to the Visual Labeling - // handshake so the selector iframe / AI flow can branch on - // `captureCssClasses` and any future `elementInteractions` - // capture toggle. elementInteractionsOptions: options, }); enableBackgroundCapture(messenger); diff --git a/packages/plugin-autocapture-browser/src/data-extractor.ts b/packages/plugin-autocapture-browser/src/data-extractor.ts index 52aeaadf0..698d92098 100644 --- a/packages/plugin-autocapture-browser/src/data-extractor.ts +++ b/packages/plugin-autocapture-browser/src/data-extractor.ts @@ -28,12 +28,6 @@ import { cssPath } from './libs/element-path'; export class DataExtractor { private readonly additionalMaskTextPatterns: RegExp[]; - // Live reference to the autocapture plugin's `ElementInteractionsOptions` - // bag. Read per-capture (not cached at construction time) so that updates - // delivered via the SDK's existing `elementInteractions` remote-config - // delivery — which mutates this bag in place, the same way `pageActions` - // is updated at `autocapture-plugin.ts:249-263` — are honored on the - // next capture without requiring SDK reinitialization. private readonly options: ElementInteractionsOptions; diagnosticsClient?: IDiagnosticsClient; @@ -96,12 +90,6 @@ export class DataExtractor { } } - // Read the effective `captureCssClasses` value live from the options - // bag on every capture. Default `true` (option `undefined` or `true`) - // preserves the previous wire-format. Setting `false` — whether at - // plugin construction or via a later in-place update to the - // already-merged `elementInteractions` options bag — omits the - // `classes` field from every hierarchy entry on the next capture. const captureCssClasses = this.options.captureCssClasses !== false; hierarchy = ancestors.map((el) => diff --git a/packages/plugin-autocapture-browser/src/hierarchy.ts b/packages/plugin-autocapture-browser/src/hierarchy.ts index 368f515b4..1c1f496f5 100644 --- a/packages/plugin-autocapture-browser/src/hierarchy.ts +++ b/packages/plugin-autocapture-browser/src/hierarchy.ts @@ -71,9 +71,6 @@ export function getElementProperties( properties.id = String(id); } - // When captureCssClasses is false, omit the `classes` field from each - // hierarchy entry entirely (never `null`, never `[]`). Default `true` - // preserves byte-equivalent behavior with previous SDK versions. if (captureCssClasses) { const classes = Array.from(element.classList); if (classes.length) { diff --git a/packages/plugin-autocapture-browser/src/libs/messenger.ts b/packages/plugin-autocapture-browser/src/libs/messenger.ts index 75177960c..6fd8269f6 100644 --- a/packages/plugin-autocapture-browser/src/libs/messenger.ts +++ b/packages/plugin-autocapture-browser/src/libs/messenger.ts @@ -83,18 +83,6 @@ export function enableVisualTagging( cssSelectorAllowlist?: string[]; actionClickAllowlist?: string[]; dataExtractor: DataExtractor; - /** - * Live reference to the autocapture plugin's effective - * `ElementInteractionsOptions` bag (post-merge of local options + - * remote-config delivery). Because mutations to this bag — the - * channel the SDK's existing `elementInteractions` remote-config - * delivery uses, mirroring the `pageActions` pattern — are visible - * through this same reference, no getter indirection is needed. - * Forwarded to the visual-tagging selector instance so the iframe - * can branch on `captureCssClasses` and any future - * `elementInteractions` capture toggle without re-extending the - * messenger contract. - */ elementInteractionsOptions?: ElementInteractionsOptions; }, ): void { @@ -145,12 +133,6 @@ export function enableVisualTagging( actionClickAllowlist, extractDataFromDataSource: dataExtractor.extractDataFromDataSource, dataExtractor, - // Live reference to the effective `elementInteractions` - // options bag. The selector iframe reads - // `captureCssClasses` (and any future `elementInteractions` - // capture toggle) directly off this object so the UI / AI - // flows stay consistent with what the SDK is actually - // capturing. elementInteractionsOptions, diagnostics: { autocapture: { diff --git a/packages/plugin-autocapture-browser/test/data-extractor.test.ts b/packages/plugin-autocapture-browser/test/data-extractor.test.ts index c04473066..dad7f161e 100644 --- a/packages/plugin-autocapture-browser/test/data-extractor.test.ts +++ b/packages/plugin-autocapture-browser/test/data-extractor.test.ts @@ -1238,55 +1238,6 @@ describe('data extractor', () => { expect(properties[constants.AMPLITUDE_EVENT_PROP_ELEMENT_CLASS]).toBe('cta primary'); }); - - // The effective value must be read live from the options bag on - // every capture, not cached at construction time, so that an - // in-place update to `options.captureCssClasses` — which is how the - // SDK's existing `elementInteractions` remote-config delivery - // surfaces an updated value, mirroring the `pageActions` pattern at - // `autocapture-plugin.ts:249-263` — is honored on the next capture - // without requiring SDK reinitialization. - test('honors in-place options-bag updates on the next capture (default → off)', () => { - const options: { captureCssClasses?: boolean } = {}; - const extractor = new DataExtractor(options); - document.getElementsByTagName('body')[0].innerHTML = HTML_FIXTURE; - - // First pass: default-on, classes captured. - const firstHierarchy = extractor.getHierarchy(document.getElementById('inner')); - expect(firstHierarchy[0]).toHaveProperty('classes', ['leaf', 'leaf-two']); - - // In-place update to the same options object (simulates remote - // config delivering `captureCssClasses: false` into the merged - // `elementInteractions` bag). - options.captureCssClasses = false; - - // Second pass: classes are now fully absent from every entry. - const secondHierarchy = extractor.getHierarchy(document.getElementById('inner')); - for (const node of secondHierarchy) { - expect(node ?? {}).not.toHaveProperty('classes'); - } - expect(JSON.stringify(secondHierarchy)).not.toContain('"classes"'); - }); - - test('honors in-place options-bag updates on the next capture (off → on)', () => { - const options: { captureCssClasses?: boolean } = { captureCssClasses: false }; - const extractor = new DataExtractor(options); - document.getElementsByTagName('body')[0].innerHTML = HTML_FIXTURE; - - // First pass: off, classes absent. - const firstHierarchy = extractor.getHierarchy(document.getElementById('inner')); - expect(JSON.stringify(firstHierarchy)).not.toContain('"classes"'); - - // In-place flip back on (e.g. remote config rolling back the - // toggle for this project). - options.captureCssClasses = true; - - // Second pass: classes are present again on every classed entry. - const secondHierarchy = extractor.getHierarchy(document.getElementById('inner')); - expect(secondHierarchy[0]).toHaveProperty('classes', ['leaf', 'leaf-two']); - expect(secondHierarchy[1]).toHaveProperty('classes', ['parent']); - expect(secondHierarchy[2]).toHaveProperty('classes', ['grandparent', 'gp2']); - }); }); }); diff --git a/packages/plugin-autocapture-browser/test/default-event-tracking-advanced.test.ts b/packages/plugin-autocapture-browser/test/default-event-tracking-advanced.test.ts index b5327f149..eddbe5ab9 100644 --- a/packages/plugin-autocapture-browser/test/default-event-tracking-advanced.test.ts +++ b/packages/plugin-autocapture-browser/test/default-event-tracking-advanced.test.ts @@ -128,12 +128,7 @@ describe('autoTrackingPlugin', () => { expect((messengerMock as any).setup).toHaveBeenCalledTimes(1); }); - // The autocapture plugin must pass a live reference to the - // effective `elementInteractions` options bag to - // `enableVisualTagging` so the selector iframe / AI flow can read - // `captureCssClasses` (and any future toggle) from it via the - // messenger handshake. - test('passes a live elementInteractionsOptions reference to enableVisualTagging', async () => { + test('passes elementInteractionsOptions to enableVisualTagging', async () => { window.opener = true; const messengerMock = { setup: jest.fn(), @@ -163,18 +158,7 @@ describe('autoTrackingPlugin', () => { const callArgs = enableVisualTaggingSpy.mock.calls[0][1] as { elementInteractionsOptions?: AnalyticsCore.ElementInteractionsOptions; }; - // The passed value is the live options bag itself (not a getter). - expect(typeof callArgs.elementInteractionsOptions).toBe('object'); - expect(callArgs.elementInteractionsOptions).not.toBeNull(); - expect(callArgs.elementInteractionsOptions!.captureCssClasses).toBe(true); - - // Mutating the plugin's options bag in place (the same mutation - // pattern the SDK's existing remote-config delivery uses for - // `elementInteractions`) surfaces through the same reference — - // this is how the iframe will pick up remote-config flips - // without having to re-handshake. - callArgs.elementInteractionsOptions!.captureCssClasses = false; - expect(callArgs.elementInteractionsOptions!.captureCssClasses).toBe(false); + expect(callArgs.elementInteractionsOptions?.captureCssClasses).toBe(true); }); test('should use custom exposureDuration from deprecated flat option', async () => { diff --git a/packages/plugin-autocapture-browser/test/hierarchy.test.ts b/packages/plugin-autocapture-browser/test/hierarchy.test.ts index ec03ec470..22a97870c 100644 --- a/packages/plugin-autocapture-browser/test/hierarchy.test.ts +++ b/packages/plugin-autocapture-browser/test/hierarchy.test.ts @@ -100,7 +100,6 @@ describe('autocapture-plugin hierarchy', () => { `; const inner = document.getElementById('inner'); - // No third argument — must behave byte-equivalent to today. expect(HierarchyUtil.getElementProperties(inner, new Set())).toEqual({ id: 'inner', index: 0,