diff --git a/packages/analytics-core/src/types/element-interactions.ts b/packages/analytics-core/src/types/element-interactions.ts index 24cf856d6..6c0397742 100644 --- a/packages/analytics-core/src/types/element-interactions.ts +++ b/packages/analytics-core/src/types/element-interactions.ts @@ -84,6 +84,15 @@ export interface ElementInteractionsOptions { messenger?: Messenger; }; + /** + * Options for integrating background capture features that rely on a Messenger + * to communicate with an external parent window. + */ + backgroundCaptureOptions?: { + enabled?: boolean; + messenger?: Messenger; + }; + /** * This has been deprecated in favor of rage clicks tracking * via frustrationInteractions. diff --git a/packages/analytics-types/src/element-interactions.ts b/packages/analytics-types/src/element-interactions.ts index 0c682da94..96d908a62 100644 --- a/packages/analytics-types/src/element-interactions.ts +++ b/packages/analytics-types/src/element-interactions.ts @@ -74,6 +74,15 @@ export interface ElementInteractionsOptions { messenger?: Messenger; }; + /** + * Options for integrating background capture features that rely on a Messenger + * to communicate with an external parent window. + */ + backgroundCaptureOptions?: { + enabled?: boolean; + messenger?: Messenger; + }; + /** * Debounce time in milliseconds for tracking events. * This is used to detect rage clicks. diff --git a/packages/plugin-autocapture-browser/package.json b/packages/plugin-autocapture-browser/package.json index 7befa7949..159ddfa80 100644 --- a/packages/plugin-autocapture-browser/package.json +++ b/packages/plugin-autocapture-browser/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "@amplitude/analytics-core": "2.35.1-feat-zoning-010526.0", + "@amplitude/rrweb-snapshot": "2.0.0-alpha.35", "rxjs": "^7.8.1", "tslib": "^2.4.1" }, diff --git a/packages/plugin-autocapture-browser/rollup.config.js b/packages/plugin-autocapture-browser/rollup.config.js index 4c31201a1..dcf0f7ea0 100644 --- a/packages/plugin-autocapture-browser/rollup.config.js +++ b/packages/plugin-autocapture-browser/rollup.config.js @@ -7,4 +7,4 @@ iife.output.name = 'amplitudeAutocapturePlugin'; if (process.env.NODE_ENV === 'development') { iife.output.sourcemap = 'inline'; } -export default [umd, iife]; +export default [umd, iife]; \ No newline at end of file diff --git a/packages/plugin-autocapture-browser/src/autocapture-plugin.ts b/packages/plugin-autocapture-browser/src/autocapture-plugin.ts index 3bcb1fa3e..53d23732e 100644 --- a/packages/plugin-autocapture-browser/src/autocapture-plugin.ts +++ b/packages/plugin-autocapture-browser/src/autocapture-plugin.ts @@ -94,6 +94,10 @@ export const autocapturePlugin = ( enabled: true, messenger: new WindowMessenger(), }, + backgroundCaptureOptions = { + enabled: true, + messenger: new WindowMessenger(), + }, } = options; options.cssSelectorAllowlist = options.cssSelectorAllowlist ?? DEFAULT_CSS_SELECTOR_ALLOWLIST; @@ -423,7 +427,6 @@ export const autocapturePlugin = ( /* istanbul ignore next */ config?.loggerProvider?.log(`${name} has been successfully added.`); - // Setup visual tagging selector if (window.opener && visualTaggingOptions.enabled) { const allowlist = (options as AutoCaptureOptionsWithDefaults).cssSelectorAllowlist; @@ -439,6 +442,15 @@ export const autocapturePlugin = ( actionClickAllowlist: actionClickAllowlist, }); } + + // Setup background capture messenger if it is not already setup for visual tagging selector + if (window.opener && backgroundCaptureOptions.enabled && !visualTaggingOptions.messenger) { + /* istanbul ignore next */ + backgroundCaptureOptions.messenger?.setup({ + dataExtractor: dataExtractor, + logger: config?.loggerProvider, + }); + } }; const execute: BrowserEnrichmentPlugin['execute'] = async (event) => { diff --git a/packages/plugin-autocapture-browser/src/constants.ts b/packages/plugin-autocapture-browser/src/constants.ts index 3efbbfc5c..44a40fa76 100644 --- a/packages/plugin-autocapture-browser/src/constants.ts +++ b/packages/plugin-autocapture-browser/src/constants.ts @@ -41,6 +41,8 @@ export const AMPLITUDE_ORIGINS_MAP = { export const AMPLITUDE_VISUAL_TAGGING_SELECTOR_SCRIPT_URL = 'https://cdn.amplitude.com/libs/visual-tagging-selector-1.0.0-alpha.js.gz'; +export const AMPLITUDE_BACKGROUND_CAPTURE_SCRIPT_URL = + 'https://cdn.amplitude.com/libs/background-capture-1.0.0-alpha.0.js.gz'; // This is the class name used by the visual tagging selector to highlight the selected element. // Should not use this class in the selector. export const AMPLITUDE_VISUAL_TAGGING_HIGHLIGHT_CLASS = 'amp-visual-tagging-selector-highlight'; diff --git a/packages/plugin-autocapture-browser/src/libs/messenger.ts b/packages/plugin-autocapture-browser/src/libs/messenger.ts index cffb960cf..f70303e06 100644 --- a/packages/plugin-autocapture-browser/src/libs/messenger.ts +++ b/packages/plugin-autocapture-browser/src/libs/messenger.ts @@ -4,9 +4,13 @@ import { AMPLITUDE_ORIGIN, AMPLITUDE_VISUAL_TAGGING_SELECTOR_SCRIPT_URL, AMPLITUDE_VISUAL_TAGGING_HIGHLIGHT_CLASS, + AMPLITUDE_BACKGROUND_CAPTURE_SCRIPT_URL, } from '../constants'; import { asyncLoadScript, generateUniqueId } from '../helpers'; import { ILogger, Messenger, ActionType } from '@amplitude/analytics-core'; +// Inline background-capture implementation (disabled) uses rrweb snapshot. +// eslint-disable-next-line import/no-extraneous-dependencies +// import { snapshot } from '@amplitude/rrweb-snapshot'; import { VERSION } from '../version'; import { DataExtractor } from '../data-extractor'; @@ -19,7 +23,11 @@ export type Action = | 'close-visual-tagging-selector' | 'element-selected' | 'track-selector-mode-changed' - | 'track-selector-moved'; + | 'track-selector-moved' + | 'initialize-background-capture' + | 'close-background-capture' + | 'background-capture-loaded' + | 'background-capture-complete'; interface InitializeVisualTaggingSelectorData { actionType: ActionType; @@ -53,6 +61,10 @@ export type ActionData = { 'element-selected': ElementSelectedData; 'track-selector-mode-changed': TrackSelectorModeChangedData; 'track-selector-moved': TrackSelectorMovedData; + 'initialize-background-capture': null | undefined; + 'close-background-capture': null | undefined; + 'background-capture-loaded': null | undefined; + 'background-capture-complete': { [key: string]: string | null }; }; export interface Message { @@ -155,7 +167,8 @@ export class WindowMessenger implements Messenger { this.endpoint = endpoint; } let amplitudeVisualTaggingSelectorInstance: any = null; - + let amplitudeBackgroundCaptureInstance: any = null; + this.logger?.debug?.('Setting up messenger'); // Attach Event Listener to listen for messages from the parent window window.addEventListener('message', (event) => { this.logger?.debug?.('Message received: ', JSON.stringify(event)); @@ -214,9 +227,27 @@ export class WindowMessenger implements Messenger { .catch(() => { this.logger?.warn('Failed to initialize visual tagging selector'); }); + } else if (action === 'initialize-background-capture') { + this.logger?.debug?.('Initializing background capture (external script)'); + asyncLoadScript(new URL(AMPLITUDE_BACKGROUND_CAPTURE_SCRIPT_URL, this.endpoint).toString()) + .then(() => { + this.logger?.debug?.('Background capture script loaded (external)'); + // eslint-disable-next-line + amplitudeBackgroundCaptureInstance = (window as any)?.amplitudeBackgroundCapture?.({ + messenger: this, + onBackgroundCapture: this.onBackgroundCapture, + }); + this.notify({ action: 'background-capture-loaded' }); + }) + .catch(() => { + this.logger?.warn('Failed to initialize background capture'); + }); } else if (action === 'close-visual-tagging-selector') { // eslint-disable-next-line amplitudeVisualTaggingSelectorInstance?.close?.(); + } else if (action === 'close-background-capture') { + // eslint-disable-next-line + amplitudeBackgroundCaptureInstance?.close?.(); } } }); @@ -236,4 +267,11 @@ export class WindowMessenger implements Messenger { this.notify({ action: 'track-selector-moved', data: properties }); } }; + + private onBackgroundCapture = (type: string, backgroundCaptureData: { [key: string]: string | number | null }) => { + if (type === 'background-capture-complete') { + this.logger?.debug?.('Background capture complete'); + this.notify({ action: 'background-capture-complete', data: backgroundCaptureData }); + } + }; } 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 00e6fa651..1d7f41bd9 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 @@ -124,23 +124,16 @@ describe('autoTrackingPlugin', () => { expect((messengerMock as any).setup).toHaveBeenCalledTimes(1); }); - test('should use custom exposureDuration when provided', async () => { - const customDuration = 500; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let intersectionCallback: (entries: any[]) => void; - - // Mock IntersectionObserver - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).IntersectionObserver = jest.fn((cb) => { - intersectionCallback = cb; - return { - observe: jest.fn(), - disconnect: jest.fn(), - }; - }); - + test('should setup background capture messenger', async () => { + window.opener = true; + const backgroundMessengerMock = { + setup: jest.fn(), + }; plugin = autocapturePlugin({ - exposureDuration: customDuration, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + visualTaggingOptions: { enabled: false, messenger: undefined as any }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + backgroundCaptureOptions: { enabled: true, messenger: backgroundMessengerMock as any }, }); const loggerProvider: Partial = { log: jest.fn(), @@ -151,48 +144,45 @@ describe('autoTrackingPlugin', () => { defaultTracking: false, loggerProvider: loggerProvider as ILogger, }; - const amplitude = createMockBrowserClient(); - await amplitude.init('API_KEY', 'USER_ID').promise; - const track = jest.spyOn(amplitude, 'track').mockImplementation(jest.fn()); - - await plugin?.setup?.(config as BrowserConfig, amplitude); - - // Create and expose an element - const element = document.createElement('button'); - element.id = 'exposure-test-button'; - document.body.appendChild(element); - - // Trigger intersection (element becomes visible) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - intersectionCallback!([ - { - isIntersecting: true, - intersectionRatio: 1.0, - target: element, - }, - ]); - - // Should not be exposed before custom duration - jest.advanceTimersByTime(customDuration - 50); - expect(track).not.toHaveBeenCalledWith('[Amplitude] Viewport Content Updated', expect.anything()); - - // Should be exposed after custom duration - jest.advanceTimersByTime(100); + const amplitude: Partial = {}; + await plugin?.setup?.(config as BrowserConfig, amplitude as BrowserClient); - // Trigger page end to flush the exposure - window.dispatchEvent(new Event('beforeunload')); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect((backgroundMessengerMock as any).setup).toHaveBeenCalledTimes(1); + }); - expect(track).toHaveBeenCalledWith( - '[Amplitude] Viewport Content Updated', - expect.objectContaining({ - '[Amplitude] Element Exposed': expect.arrayContaining(['button#exposure-test-button']), - }), - ); + test('should not setup background capture messenger when visual tagging messenger is set', async () => { + window.opener = true; + const visualMessengerMock = { + setup: jest.fn(), + }; + const backgroundMessengerMock = { + setup: jest.fn(), + }; + plugin = autocapturePlugin({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + visualTaggingOptions: { enabled: true, messenger: visualMessengerMock as any }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + backgroundCaptureOptions: { enabled: true, messenger: backgroundMessengerMock as any }, + }); + 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); - // Cleanup - element.remove(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).IntersectionObserver = undefined; + // Visual tagging messenger should be set up + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect((visualMessengerMock as any).setup).toHaveBeenCalledTimes(1); + // Background capture messenger should NOT be set up (condition: !visualTaggingOptions.messenger) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect((backgroundMessengerMock as any).setup).toHaveBeenCalledTimes(0); }); }); @@ -292,7 +282,6 @@ describe('autoTrackingPlugin', () => { '[Amplitude] Element Text': 'my-link-text', '[Amplitude] Element Aria Label': 'my-link', '[Amplitude] Element Parent Label': 'my-h2-text', - '[Amplitude] Element Path': 'a#my-link-id', '[Amplitude] Page URL': 'https://www.amplitude.com/unit-test', '[Amplitude] Viewport Height': 768, '[Amplitude] Viewport Width': 1024, @@ -478,7 +467,6 @@ describe('autoTrackingPlugin', () => { '[Amplitude] Element Text': 'my-link-text', '[Amplitude] Element Aria Label': 'my-link', '[Amplitude] Element Parent Label': 'my-h2-text', - '[Amplitude] Element Path': 'a#my-link-id', '[Amplitude] Page URL': 'https://www.amplitude.com/unit-test', '[Amplitude] Viewport Height': 768, '[Amplitude] Viewport Width': 1024, @@ -579,7 +567,6 @@ describe('autoTrackingPlugin', () => { '[Amplitude] Element Text': 'submit', '[Amplitude] Element Aria Label': 'my-button', '[Amplitude] Element Parent Label': 'my-h2-text', - '[Amplitude] Element Path': 'button#my-button-id', '[Amplitude] Page URL': 'https://www.amplitude.com/unit-test', '[Amplitude] Viewport Height': 768, '[Amplitude] Viewport Width': 1024, @@ -651,7 +638,6 @@ describe('autoTrackingPlugin', () => { '[Amplitude] Element Text': 'submit', '[Amplitude] Element Aria Label': 'my-button', '[Amplitude] Element Parent Label': 'my-h2-text', - '[Amplitude] Element Path': 'button#my-button-id', '[Amplitude] Page URL': 'https://www.amplitude.com/unit-test', '[Amplitude] Viewport Height': 768, '[Amplitude] Viewport Width': 1024, @@ -1012,7 +998,6 @@ describe('autoTrackingPlugin', () => { '[Amplitude] Element Tag': 'button', '[Amplitude] Element Text': 'submit', '[Amplitude] Element Parent Label': 'my-h2-text', - '[Amplitude] Element Path': 'button#my-button-id', '[Amplitude] Page URL': 'https://www.amplitude.com/unit-test', '[Amplitude] Viewport Height': 768, '[Amplitude] Viewport Width': 1024, @@ -1457,308 +1442,6 @@ describe('autoTrackingPlugin', () => { expect(track).toHaveBeenCalledTimes(6); }); }); - - describe('page view ID handling', () => { - // Tests for data-extractor.ts lines 160-170 which use optional chaining - // (window?.sessionStorage?.getItem) to safely access sessionStorage - test('should not throw error when sessionStorage is deleted', async () => { - const config: Partial = { - defaultTracking: false, - loggerProvider: loggerProvider, - }; - await plugin?.setup?.(config as BrowserConfig, instance); - - // Save original sessionStorage - const originalSessionStorage = window.sessionStorage; - - // Delete sessionStorage temporarily - tests window?.sessionStorage when sessionStorage is undefined - // eslint-disable-next-line @typescript-eslint/no-explicit-any - delete (window as any).sessionStorage; - - // trigger click event - const link = document.createElement('a'); - link.setAttribute('id', 'test-link-id'); - link.setAttribute('class', 'test-link-class'); - link.href = 'https://www.amplitude.com/test'; - link.text = 'test-link-text'; - document.body.appendChild(link); - - document.getElementById('test-link-id')?.dispatchEvent(new Event('click')); - jest.advanceTimersByTime(TESTING_DEBOUNCE_TIME + 3); - - expect(track).toHaveBeenCalledTimes(1); - // Verify that page view ID is not included in the event properties - expect(track).toHaveBeenNthCalledWith( - 1, - '[Amplitude] Element Clicked', - expect.not.objectContaining({ - '[Amplitude] Page View ID': expect.anything(), - }), - ); - - // Restore sessionStorage - window.sessionStorage = originalSessionStorage; - - // Cleanup - document.getElementById('test-link-id')?.remove(); - }); - - test('should not throw error when window.sessionStorage is undefined', async () => { - const config: Partial = { - defaultTracking: false, - loggerProvider: loggerProvider, - }; - await plugin?.setup?.(config as BrowserConfig, instance); - - // Save original window.sessionStorage - const originalSessionStorage = window.sessionStorage; - - // Set window.sessionStorage to undefined - tests optional chaining when window?.sessionStorage is undefined - Object.defineProperty(window, 'sessionStorage', { - value: undefined, - writable: true, - configurable: true, - }); - - // trigger click event - const link = document.createElement('a'); - link.setAttribute('id', 'test-link-id-2'); - link.setAttribute('class', 'test-link-class'); - link.href = 'https://www.amplitude.com/test'; - link.text = 'test-link-text'; - document.body.appendChild(link); - - document.getElementById('test-link-id-2')?.dispatchEvent(new Event('click')); - jest.advanceTimersByTime(TESTING_DEBOUNCE_TIME + 3); - - expect(track).toHaveBeenCalledTimes(1); - // Verify that page view ID is not included in the event properties - expect(track).toHaveBeenNthCalledWith( - 1, - '[Amplitude] Element Clicked', - expect.not.objectContaining({ - '[Amplitude] Page View ID': expect.anything(), - }), - ); - - // Restore sessionStorage - window.sessionStorage = originalSessionStorage; - - // Cleanup - document.getElementById('test-link-id-2')?.remove(); - }); - }); - }); - - describe('Viewport Content Updated Tracking', () => { - const API_KEY = 'API_KEY'; - const USER_ID = 'USER_ID'; - - let instance = createMockBrowserClient(); - let track: jest.SpyInstance; - let loggerProvider: ILogger; - // eslint-disable-next-line @typescript-eslint/unbound-method - const originalPushState = history.pushState; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let intersectionCallback: (entries: any[]) => void; - - beforeEach(async () => { - // Ensure navigation API is not present to test fallback (pushState proxy) - Object.defineProperty(window, 'navigation', { - value: undefined, - writable: true, - configurable: true, - }); - - loggerProvider = { - log: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - debug: jest.fn(), - } as unknown as ILogger; - - // Mock IntersectionObserver - (window as any).IntersectionObserver = jest.fn((cb) => { - intersectionCallback = cb; - return { - observe: jest.fn(), - disconnect: jest.fn(), - }; - }); - - instance = createMockBrowserClient(); - await instance.init(API_KEY, USER_ID).promise; - track = jest.spyOn(instance, 'track').mockImplementation(jest.fn()); - }); - - afterEach(async () => { - if (originalPushState) { - history.pushState = originalPushState; - } - (window as any).IntersectionObserver = undefined; - }); - - test('should track [Amplitude] Viewport Content Updated on beforeunload', async () => { - const config: Partial = { - defaultTracking: false, - loggerProvider: loggerProvider, - }; - await plugin?.setup?.(config as BrowserConfig, instance); - - window.dispatchEvent(new Event('beforeunload')); - - expect(track).toHaveBeenCalledWith( - '[Amplitude] Viewport Content Updated', - expect.objectContaining({ - '[Amplitude] Page URL': expect.any(String), - '[Amplitude] Viewport Height': expect.any(Number), - '[Amplitude] Viewport Width': expect.any(Number), - }), - ); - }); - - test('should not track duplicate [Amplitude] Viewport Content Updated events on multiple beforeunload', async () => { - const config: Partial = { - defaultTracking: false, - loggerProvider: loggerProvider, - }; - await plugin?.setup?.(config as BrowserConfig, instance); - - window.dispatchEvent(new Event('beforeunload')); - window.dispatchEvent(new Event('beforeunload')); - - expect(track).toHaveBeenCalledTimes(1); - }); - - test('should track [Amplitude] Viewport Content Updated on history.pushState and reset state', async () => { - const config: Partial = { - defaultTracking: false, - loggerProvider: loggerProvider, - }; - await plugin?.setup?.(config as BrowserConfig, instance); - - // history.pushState is proxied. - history.pushState({}, 'test', '/new-page'); - - expect(track).toHaveBeenCalledWith('[Amplitude] Viewport Content Updated', expect.any(Object)); - - jest.advanceTimersByTime(1000); - // change scroll depth to trigger a new viewport content updated event - Object.defineProperty(window, 'scrollY', { value: 100, writable: true }); - Object.defineProperty(window, 'pageYOffset', { value: 100, writable: true }); - window.dispatchEvent(new Event('scroll')); - - // Verify it can fire again (pageViewEndFired should be reset to false by the proxy) - history.pushState({}, 'test', '/another-page'); - expect(track).toHaveBeenCalledTimes(2); - }); - - test('should track [Amplitude] Viewport Content Updated on popstate event', async () => { - const config: Partial = { - defaultTracking: false, - loggerProvider: loggerProvider, - }; - await plugin?.setup?.(config as BrowserConfig, instance); - - // Simulate popstate event - window.dispatchEvent(new Event('popstate')); - - expect(track).toHaveBeenCalledWith('[Amplitude] Viewport Content Updated', expect.any(Object)); - }); - - test('should flush Viewport Content Updated event when exposure buffer limit is reached', async () => { - const config: Partial = { - defaultTracking: false, - loggerProvider: loggerProvider, - }; - await plugin?.setup?.(config as BrowserConfig, instance); - - // Mock finder to avoid DOM dependencies for path generation - // We can't easily mock the finder inside the module from here without jest.mock hoisting. - // Instead, we rely on elements having IDs which finder uses. - - // We need to trigger exposure for enough unique elements to exceed 18000 chars. - // exposedString = JSON.stringify(["#id1", "#id2", ...]) - // A simple id like "#e-1" is 4 chars. Quotes and comma add 3 chars. ~7 chars per element. - // 18000 / 7 = ~2571 elements. - - const entries: any[] = []; - const elements: HTMLElement[] = []; - - // Create a batch of elements - // Make ids long to reduce iteration count - const longIdPrefix = 'e'.repeat(100); // 100 chars - const count = 180; // 180 * 100 = 18000. - - for (let i = 0; i < count; i++) { - const el = document.createElement('div'); - el.id = `${longIdPrefix}-${i}`; - document.body.appendChild(el); - elements.push(el); - - entries.push({ - isIntersecting: true, - intersectionRatio: 1.0, - target: el, - }); - } - - // Trigger intersection - intersectionCallback(entries); - - // Exposure has a debounce or delay? - // trackExposure uses observables which might be async or use internal logic. - // trackExposure has an internal buffer/timer. - // trackExposure implementation waits for element to be visible for some time (defaults 0?) - // Actually trackExposure defaults to 0 debounce/timeout? - // Let's check track-exposure.ts logic. It sets a timeout. - // We need to advance timers. - - // The default threshold is likely 0 if not configured? - // No, trackExposure has hardcoded logic or uses options. - // Looking at track-exposure.ts: - // It sets a timeout of 2000ms (hardcoded in the file I saw previously?) - // Or it reads options? - - // Let's verify with a smaller test first or assume 2s. - jest.advanceTimersByTime(2000); // Wait for exposure to be confirmed - - // Now onExposure should be called for each element. - // onExposure adds to set and checks size. - - // We expect at least one track call - expect(track).toHaveBeenCalledWith('[Amplitude] Viewport Content Updated', expect.any(Object)); - - // Cleanup - elements.forEach((el) => el.remove()); - }); - - test('should include scroll and exposure state in Viewport Content Updated event', async () => { - const config: Partial = { - defaultTracking: false, - loggerProvider: loggerProvider, - }; - await plugin?.setup?.(config as BrowserConfig, instance); - - // Trigger a scroll to update state - Object.defineProperty(window, 'scrollX', { value: 100, writable: true }); - Object.defineProperty(window, 'scrollY', { value: 200, writable: true }); - Object.defineProperty(window, 'pageXOffset', { value: 100, writable: true }); - Object.defineProperty(window, 'pageYOffset', { value: 200, writable: true }); - window.dispatchEvent(new Event('scroll')); - - // Trigger page view end - window.dispatchEvent(new Event('beforeunload')); - - expect(track).toHaveBeenCalledWith( - '[Amplitude] Viewport Content Updated', - expect.objectContaining({ - '[Amplitude] Max Page X': 100 + 1024, - '[Amplitude] Max Page Y': 200 + 768, - '[Amplitude] Element Exposed': expect.any(Array), - }), - ); - }); }); describe('teardown', () => { diff --git a/yarn.lock b/yarn.lock index 35cc04d98..1c618f886 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,11 +2,56 @@ # yarn lockfile v1 +"@amplitude/analytics-browser@^2.17.6", "@amplitude/analytics-browser@^2.6.3-beta.0": + version "2.33.4" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-browser/-/analytics-browser-2.33.4.tgz#708558610dd6f8bb3fb0fed6a148f365caab9e3b" + integrity sha512-5oeZ3fsxbXiE6S7Jq/bsYn10DJ+IPSY1dC08PO2kD9cfaviWtXVrehSwThitEZGKHGs4NeJXCGS1xAhOLR2g0g== + dependencies: + "@amplitude/analytics-core" "2.36.0" + "@amplitude/plugin-autocapture-browser" "1.18.6" + "@amplitude/plugin-network-capture-browser" "1.7.6" + "@amplitude/plugin-page-url-enrichment-browser" "0.5.12" + "@amplitude/plugin-page-view-tracking-browser" "2.6.9" + "@amplitude/plugin-web-vitals-browser" "1.1.7" + tslib "^2.4.1" + +"@amplitude/analytics-client-common@>=1 <3": + version "2.4.22" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-client-common/-/analytics-client-common-2.4.22.tgz#5453e30bc889a2c9320e944205bfa0dfdee0b2a6" + integrity sha512-hRC45DOns0JA96k8Ph7oqMpZsYvtMZeth2i78n0Bh4bOx2D4XtHum3Ok49jvE3IFve/sfV7NEgBNgoYKHfiwvQ== + dependencies: + "@amplitude/analytics-connector" "^1.4.8" + "@amplitude/analytics-core" "2.36.0" + "@amplitude/analytics-types" "2.11.1" + tslib "^2.4.1" + "@amplitude/analytics-connector@^1.4.8", "@amplitude/analytics-connector@^1.6.4": version "1.6.4" resolved "https://registry.yarnpkg.com/@amplitude/analytics-connector/-/analytics-connector-1.6.4.tgz#8a811ff5c8ee46bdfea0e8f61c7578769b5778ed" integrity sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q== +"@amplitude/analytics-core@2.36.0", "@amplitude/analytics-core@>=1 <3", "@amplitude/analytics-core@^2.10.0", "@amplitude/analytics-core@^2.12.0": + version "2.36.0" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-core/-/analytics-core-2.36.0.tgz#ebb3f2e73bcfd131860f60c3be144b5dd45e4033" + integrity sha512-VPkqVK7PZBwkatD22Xu0kshtLeM8bd6KjCsFcnje0FA/LHgixYw1O4ihWpPlUzDNMIXSb2+opN3SkINImmBOnQ== + dependencies: + "@amplitude/analytics-connector" "^1.6.4" + tslib "^2.4.1" + zen-observable-ts "^1.1.0" + +"@amplitude/analytics-node@^1.3.9": + version "1.5.32" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-node/-/analytics-node-1.5.32.tgz#9bda617a60f671c276279b6184f8e02c5c564413" + integrity sha512-VkZ1uPqxXwtMaGRn/LLCNkSHrIPj0mJcCzh3YVKI17vK5Qr9PDg9A15+4b5rRpOCRBKtRsXkBHUHaRicWk0CVA== + dependencies: + "@amplitude/analytics-core" "2.36.0" + tslib "^2.4.1" + +"@amplitude/analytics-types@2.11.1": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@amplitude/analytics-types/-/analytics-types-2.11.1.tgz#7af47830a9d55f32dc51e455e7bd4c266b96250d" + integrity sha512-wFEgb0t99ly2uJKm5oZ28Lti0Kh5RecR5XBkwfUpDzn84IoCIZ8GJTsMw/nThu8FZFc7xFDA4UAt76zhZKrs9A== + "@amplitude/analytics-types@^1.0.0", "@amplitude/analytics-types@^1.3.4": version "1.4.0" resolved "https://registry.yarnpkg.com/@amplitude/analytics-types/-/analytics-types-1.4.0.tgz#63f84e5ea5e26beeb06745732063e3787194f0d2" @@ -53,6 +98,47 @@ base64-js "1.5.1" unfetch "4.1.0" +"@amplitude/plugin-autocapture-browser@1.18.6": + version "1.18.6" + resolved "https://registry.yarnpkg.com/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.18.6.tgz#89914e978c4e4a1a31f67091a1762f7f191a9369" + integrity sha512-8oZ0jGHjs9Sm98L7l2X5nVjT/qAv+ezk/eibYdHiwA10haHRjXc+v4cFuGeboQkSd87gWvD4LyA7iamVUUak/Q== + dependencies: + "@amplitude/analytics-core" "2.36.0" + tslib "^2.4.1" + +"@amplitude/plugin-network-capture-browser@1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.7.6.tgz#3f627d145341374e4f499caacfeb90b135b26816" + integrity sha512-FJMdpeOV9e4+TYUfUTSIuBuBU4dLRwB7Qq/tFbFHEogAH8NFcsYKxe0rAWmTqMTmKxb2SHTIEC35D+aWVJzWCQ== + dependencies: + "@amplitude/analytics-core" "2.36.0" + tslib "^2.4.1" + +"@amplitude/plugin-page-url-enrichment-browser@0.5.12": + version "0.5.12" + resolved "https://registry.yarnpkg.com/@amplitude/plugin-page-url-enrichment-browser/-/plugin-page-url-enrichment-browser-0.5.12.tgz#d2e5e6c02e3af1aeed9c91d6de84c6bc3ddbc31c" + integrity sha512-FMPaY+apoyULJSzTMdz2UQ0c8Ry3J/m1yD9sjsRy2VGhbXyLFV5zrfcHkiIZAtDHy2sVpsv130j1eGZIK2aqZw== + dependencies: + "@amplitude/analytics-core" "2.36.0" + tslib "^2.4.1" + +"@amplitude/plugin-page-view-tracking-browser@2.6.9": + version "2.6.9" + resolved "https://registry.yarnpkg.com/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.6.9.tgz#d9ab990841264f72e69f59f8ccd0771cb1f0854b" + integrity sha512-LfV+4t8V7Kq6TKecaggC2rOszE9sVTs73xPok1UXGvlvVkY+KaEc9ngkansBOKCfCU7inNaIMlGRj1YZDrEjjA== + dependencies: + "@amplitude/analytics-core" "2.36.0" + tslib "^2.4.1" + +"@amplitude/plugin-web-vitals-browser@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-1.1.7.tgz#ec3e3763bf3b848bce0751f4ac178ccb903a3f12" + integrity sha512-n1zOsE1RFE3y2IN1OUKTZYQnR7NZMATarHjBsf/tJ+6fQ2g5QDwyTRLzBHmdUcsLe559+ek9QTtIhXmbBOXR3Q== + dependencies: + "@amplitude/analytics-core" "2.36.0" + tslib "^2.4.1" + web-vitals "5.1.0" + "@amplitude/rrdom@^2.0.0-alpha.34": version "2.0.0-alpha.34" resolved "https://registry.yarnpkg.com/@amplitude/rrdom/-/rrdom-2.0.0-alpha.34.tgz#b36ec8d43d5a5bdfdd5094ef27836d38294b077f" @@ -81,6 +167,13 @@ "@amplitude/rrweb" "^2.0.0-alpha.34" "@amplitude/rrweb-types" "^2.0.0-alpha.34" +"@amplitude/rrweb-snapshot@2.0.0-alpha.35": + version "2.0.0-alpha.35" + resolved "https://registry.yarnpkg.com/@amplitude/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.35.tgz#3b5d0326a3d2494b35da5e29d3fc4d0d1bc1fffd" + integrity sha512-n55AdmlRNZ7XuOlCRmSjH2kyyHS1oe5haUS+buxqjfQcamUtam+dSnP+6N1E8dLxIDjynJnbrCOC+8xvenpl1A== + dependencies: + postcss "^8.4.38" + "@amplitude/rrweb-snapshot@^2.0.0-alpha.34": version "2.0.0-alpha.34" resolved "https://registry.yarnpkg.com/@amplitude/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.34.tgz#1aee396336b1855539cb844013ffb974da7c7749"