diff --git a/packages/plugin-autocapture-browser/src/autocapture-plugin.ts b/packages/plugin-autocapture-browser/src/autocapture-plugin.ts index 97bf869a0..6c109415f 100644 --- a/packages/plugin-autocapture-browser/src/autocapture-plugin.ts +++ b/packages/plugin-autocapture-browser/src/autocapture-plugin.ts @@ -50,6 +50,7 @@ export enum ObservablesEnum { // ErrorObservable = 'errorObservable', NavigateObservable = 'navigateObservable', MutationObservable = 'mutationObservable', + VisibilityChangeObservable = 'visibilityChangeObservable', } export interface AllWindowObservables { @@ -58,6 +59,7 @@ export interface AllWindowObservables { // [ObservablesEnum.ErrorObservable]: Observable>; [ObservablesEnum.NavigateObservable]: Observable> | undefined; [ObservablesEnum.MutationObservable]: Observable>; + [ObservablesEnum.VisibilityChangeObservable]?: Observable>; } export const autocapturePlugin = (options: ElementInteractionsOptions = {}): BrowserEnrichmentPlugin => { diff --git a/packages/plugin-autocapture-browser/src/autocapture/track-dead-click.ts b/packages/plugin-autocapture-browser/src/autocapture/track-dead-click.ts index ba9b02bf2..eeb3e073a 100644 --- a/packages/plugin-autocapture-browser/src/autocapture/track-dead-click.ts +++ b/packages/plugin-autocapture-browser/src/autocapture/track-dead-click.ts @@ -27,7 +27,7 @@ export function trackDeadClick({ getEventProperties: (actionType: ActionType, element: Element) => Record; shouldTrackDeadClick: shouldTrackEvent; }) { - const { clickObservable, mutationObservable, navigateObservable } = allObservables; + const { clickObservable, mutationObservable, navigateObservable, visibilityChangeObservable } = allObservables; const filteredClickObservable = clickObservable.pipe( filter(filterOutNonTrackableEvents), @@ -38,11 +38,16 @@ export function trackDeadClick({ ); const changeObservables: Array< - AllWindowObservables[ObservablesEnum.MutationObservable] | AllWindowObservables[ObservablesEnum.NavigateObservable] + | AllWindowObservables[ObservablesEnum.MutationObservable] + | AllWindowObservables[ObservablesEnum.NavigateObservable] + | AllWindowObservables[ObservablesEnum.VisibilityChangeObservable] > = [mutationObservable]; if (navigateObservable) { changeObservables.push(navigateObservable); } + if (visibilityChangeObservable) { + changeObservables.push(visibilityChangeObservable); + } const mutationOrNavigate = merge(...changeObservables); const actionClicks = filteredClickObservable.pipe( diff --git a/packages/plugin-autocapture-browser/src/frustration-plugin.ts b/packages/plugin-autocapture-browser/src/frustration-plugin.ts index ff111ccc8..3500e7ecf 100644 --- a/packages/plugin-autocapture-browser/src/frustration-plugin.ts +++ b/packages/plugin-autocapture-browser/src/frustration-plugin.ts @@ -10,7 +10,7 @@ import { } from '@amplitude/analytics-core'; import * as constants from './constants'; import { fromEvent, map, Observable, Subscription, share } from 'rxjs'; -import { createShouldTrackEvent, ElementBasedTimestampedEvent, NavigateEvent } from './helpers'; +import { createShouldTrackEvent, ElementBasedTimestampedEvent, NavigateEvent, TimestampedEvent } from './helpers'; import { trackDeadClick } from './autocapture/track-dead-click'; import { trackRageClicks } from './autocapture/track-rage-click'; import { AllWindowObservables, ObservablesEnum } from './autocapture-plugin'; @@ -63,6 +63,15 @@ export const frustrationPlugin = (options: FrustrationInteractionsOptions = {}): ); } + const visibilityChangeObservable = fromEvent>(document, 'visibilitychange').pipe( + map((visibilityChange) => { + console.log('visibilityChange', visibilityChange); + dataExtractor.addAdditionalEventProperties(visibilityChange, 'visibilitychange', [], dataAttributePrefix); + return visibilityChange; + }), + share(), + ); + // Track DOM Mutations const enrichedMutationObservable = createMutationObservable().pipe( map((mutation) => @@ -76,6 +85,7 @@ export const frustrationPlugin = (options: FrustrationInteractionsOptions = {}): [ObservablesEnum.ChangeObservable]: new Observable>(), // Empty observable since we don't need change events [ObservablesEnum.NavigateObservable]: navigateObservable, [ObservablesEnum.MutationObservable]: enrichedMutationObservable, + [ObservablesEnum.VisibilityChangeObservable]: visibilityChangeObservable, }; }; diff --git a/packages/plugin-autocapture-browser/src/helpers.ts b/packages/plugin-autocapture-browser/src/helpers.ts index 8b549cbe1..c51af1d6d 100644 --- a/packages/plugin-autocapture-browser/src/helpers.ts +++ b/packages/plugin-autocapture-browser/src/helpers.ts @@ -228,7 +228,7 @@ export type AutoCaptureOptionsWithDefaults = Required< export type BaseTimestampedEvent = { event: T; timestamp: number; - type: 'rage' | 'click' | 'change' | 'error' | 'navigate' | 'mutation'; + type: 'rage' | 'click' | 'change' | 'error' | 'navigate' | 'visibilitychange' | 'mutation'; }; // Specific types for events with targetElementProperties diff --git a/packages/plugin-autocapture-browser/test/autocapture-plugin/frustration-plugin.test.ts b/packages/plugin-autocapture-browser/test/autocapture-plugin/frustration-plugin.test.ts index 4b6b7df37..e3e9b2545 100644 --- a/packages/plugin-autocapture-browser/test/autocapture-plugin/frustration-plugin.test.ts +++ b/packages/plugin-autocapture-browser/test/autocapture-plugin/frustration-plugin.test.ts @@ -239,6 +239,7 @@ describe('frustrationPlugin', () => { expect(observables).toHaveProperty('mutationObservable'); expect(observables).toHaveProperty('navigateObservable'); expect(observables).toHaveProperty('changeObservable'); + expect(observables).toHaveProperty('visibilityChangeObservable'); // Test click observable const clickSpy = jest.fn(); @@ -283,6 +284,13 @@ describe('frustrationPlugin', () => { // Verify mutation was captured expect(mutationSpy).toHaveBeenCalled(); + // Test visibility change observable + const visibilityChangeSpy = jest.fn(); + const visibilityChangeSubscription = observables.visibilityChangeObservable.subscribe(visibilityChangeSpy); + document.dispatchEvent(new Event('visibilitychange')); + expect(visibilityChangeSpy).toHaveBeenCalled(); + visibilityChangeSubscription.unsubscribe(); + // Cleanup mutationSubscription.unsubscribe(); document.body.removeChild(container); diff --git a/packages/plugin-autocapture-browser/test/autocapture-plugin/track-dead-click.test.ts b/packages/plugin-autocapture-browser/test/autocapture-plugin/track-dead-click.test.ts index 028926adf..5448af383 100644 --- a/packages/plugin-autocapture-browser/test/autocapture-plugin/track-dead-click.test.ts +++ b/packages/plugin-autocapture-browser/test/autocapture-plugin/track-dead-click.test.ts @@ -13,6 +13,7 @@ describe('trackDeadClick', () => { let clickObservable: Subject; let mutationObservable: Subject; let navigateObservable: Subject; + let visibilityChangeObservable: Subject; let allObservables: AllWindowObservables; let shouldTrackDeadClick: jest.Mock; let getEventProperties: jest.Mock; @@ -30,11 +31,13 @@ describe('trackDeadClick', () => { clickObservable = new Subject(); mutationObservable = new Subject(); navigateObservable = new Subject(); + visibilityChangeObservable = new Subject(); allObservables = { [ObservablesEnum.ClickObservable]: clickObservable, [ObservablesEnum.ChangeObservable]: new Subject(), [ObservablesEnum.NavigateObservable]: navigateObservable, [ObservablesEnum.MutationObservable]: mutationObservable, + [ObservablesEnum.VisibilityChangeObservable]: visibilityChangeObservable, }; shouldTrackDeadClick = jest.fn().mockReturnValue(true); getEventProperties = jest.fn().mockReturnValue({ id: 'test-element' }); @@ -155,6 +158,41 @@ describe('trackDeadClick', () => { }, 2); }); + it('should not track when document visibility changes after click', (done) => { + const subscription = trackDeadClick({ + amplitude: mockAmplitude, + allObservables, + getEventProperties, + shouldTrackDeadClick, + }); + + // Create a mock element + const mockElement = document.createElement('div'); + + // Simulate a click + clickObservable.next({ + event: { + target: mockElement, + clientX: 100, + clientY: 100, + }, + timestamp: Date.now(), + closestTrackedAncestor: mockElement, + targetElementProperties: { id: 'test-element' }, + }); + + setTimeout(() => { + visibilityChangeObservable.next({}); + }, 1); + + // Wait for the dead click timeout + setTimeout(() => { + expect(mockAmplitude.track).not.toHaveBeenCalled(); + subscription.unsubscribe(); + done(); + }, 100); + }); + it('should not track elements that are not in the allowed list', (done) => { shouldTrackDeadClick.mockReturnValue(false); diff --git a/packages/plugin-autocapture-browser/test/autocapture-plugin/track-rage-click.test.ts b/packages/plugin-autocapture-browser/test/autocapture-plugin/track-rage-click.test.ts index 1aa7d41a3..391f07efa 100644 --- a/packages/plugin-autocapture-browser/test/autocapture-plugin/track-rage-click.test.ts +++ b/packages/plugin-autocapture-browser/test/autocapture-plugin/track-rage-click.test.ts @@ -26,6 +26,7 @@ describe('trackRageClicks', () => { [ObservablesEnum.ChangeObservable]: new Subject(), [ObservablesEnum.NavigateObservable]: new Subject(), [ObservablesEnum.MutationObservable]: new Subject(), + [ObservablesEnum.VisibilityChangeObservable]: new Subject(), }; shouldTrackRageClick = jest.fn().mockReturnValue(true); }); diff --git a/test-server/autocapture/element-interactions.html b/test-server/autocapture/element-interactions.html index 81949d47e..760d262c6 100644 --- a/test-server/autocapture/element-interactions.html +++ b/test-server/autocapture/element-interactions.html @@ -112,6 +112,11 @@

Clickable Elements Reference

+ + New Page (no dead click) + Yes + Always, unless tabindex="-1" or disabled + Link Yes