diff --git a/packages/plugin-autocapture-browser/src/autocapture/track-action-click.ts b/packages/plugin-autocapture-browser/src/autocapture/track-action-click.ts index db0335380..3d57ea812 100644 --- a/packages/plugin-autocapture-browser/src/autocapture/track-action-click.ts +++ b/packages/plugin-autocapture-browser/src/autocapture/track-action-click.ts @@ -1,4 +1,4 @@ -import { AllWindowObservables, AutoCaptureOptionsWithDefaults } from 'src/autocapture-plugin'; +import { AllWindowObservables, AutoCaptureOptionsWithDefaults } from '../autocapture-plugin'; import { BrowserClient, ActionType, merge, asyncMap } from '@amplitude/analytics-core'; import { ElementBasedTimestampedEvent, diff --git a/packages/plugin-autocapture-browser/src/autocapture/track-change.ts b/packages/plugin-autocapture-browser/src/autocapture/track-change.ts index bf462ee98..6ab31402e 100644 --- a/packages/plugin-autocapture-browser/src/autocapture/track-change.ts +++ b/packages/plugin-autocapture-browser/src/autocapture/track-change.ts @@ -1,7 +1,11 @@ -import { AllWindowObservables } from 'src/autocapture-plugin'; -import { ElementBasedTimestampedEvent, type evaluateTriggersFn } from 'src/helpers'; +import { AllWindowObservables } from '../autocapture-plugin'; +import { + filterOutNonTrackableEvents, + shouldTrackEvent, + ElementBasedTimestampedEvent, + type evaluateTriggersFn, +} from '../helpers'; import { BrowserClient, ActionType } from '@amplitude/analytics-core'; -import { filterOutNonTrackableEvents, shouldTrackEvent } from '../helpers'; import { AMPLITUDE_ELEMENT_CHANGED_EVENT } from '../constants'; export function trackChange({ diff --git a/packages/plugin-autocapture-browser/src/autocapture/track-click.ts b/packages/plugin-autocapture-browser/src/autocapture/track-click.ts index e73f48967..a0b24dcfb 100644 --- a/packages/plugin-autocapture-browser/src/autocapture/track-click.ts +++ b/packages/plugin-autocapture-browser/src/autocapture/track-click.ts @@ -1,5 +1,5 @@ -import { AllWindowObservables } from 'src/autocapture-plugin'; -import { type evaluateTriggersFn } from 'src/helpers'; +import { AllWindowObservables } from '../autocapture-plugin'; +import { ElementBasedEvent, ElementBasedTimestampedEvent, type evaluateTriggersFn } from '../helpers'; import { Observable, BrowserClient } from '@amplitude/analytics-core'; import { filterOutNonTrackableEvents, shouldTrackEvent } from '../helpers'; import { AMPLITUDE_ELEMENT_CLICKED_EVENT } from '../constants'; @@ -28,7 +28,7 @@ export function trackClicks({ const clicks: Observable ? U : never> = clickObservableFiltered; - return clicks.subscribe((click) => { + return clicks.subscribe((click: ElementBasedTimestampedEvent) => { /* istanbul ignore next */ amplitude?.track(AMPLITUDE_ELEMENT_CLICKED_EVENT, click.targetElementProperties); }); 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 0c1fa0321..37dc98a67 100644 --- a/packages/plugin-autocapture-browser/src/autocapture/track-dead-click.ts +++ b/packages/plugin-autocapture-browser/src/autocapture/track-dead-click.ts @@ -1,4 +1,4 @@ -import { AllWindowObservables } from 'src/frustration-plugin'; +import { AllWindowObservables } from '../frustration-plugin'; import { BrowserClient, ActionType, merge, asyncMap } from '@amplitude/analytics-core'; import { ElementBasedTimestampedEvent, filterOutNonTrackableEvents, shouldTrackEvent } from '../helpers'; import { AMPLITUDE_ELEMENT_DEAD_CLICKED_EVENT } from '../constants'; diff --git a/packages/plugin-autocapture-browser/src/autocapture/track-rage-click.ts b/packages/plugin-autocapture-browser/src/autocapture/track-rage-click.ts index 8bfd94150..0af617b02 100644 --- a/packages/plugin-autocapture-browser/src/autocapture/track-rage-click.ts +++ b/packages/plugin-autocapture-browser/src/autocapture/track-rage-click.ts @@ -1,4 +1,4 @@ -import { AllWindowObservables } from 'src/frustration-plugin'; +import { AllWindowObservables } from '../frustration-plugin'; import { BrowserClient, asyncMap, @@ -48,11 +48,11 @@ type RageClickEvent = { }; function addCoordinates(regionBox: ClickRegionBoundingBox, click: ClickEvent) { - const { clientX, clientY } = click.event as MouseEvent; - regionBox.yMin = Math.min(regionBox.yMin ?? clientY, clientY); - regionBox.yMax = Math.max(regionBox.yMax ?? clientY, clientY); - regionBox.xMin = Math.min(regionBox.xMin ?? clientX, clientX); - regionBox.xMax = Math.max(regionBox.xMax ?? clientX, clientX); + const { pageX, pageY } = click.event as MouseEvent; + regionBox.yMin = Math.min(regionBox.yMin ?? pageY, pageY); + regionBox.yMax = Math.max(regionBox.yMax ?? pageY, pageY); + regionBox.xMin = Math.min(regionBox.xMin ?? pageX, pageX); + regionBox.xMax = Math.max(regionBox.xMax ?? pageX, pageX); regionBox.isOutOfBounds = regionBox.yMax - regionBox.yMin > RAGE_CLICK_OUT_OF_BOUNDS_THRESHOLD || regionBox.xMax - regionBox.xMin > RAGE_CLICK_OUT_OF_BOUNDS_THRESHOLD; @@ -71,8 +71,8 @@ function getRageClickAnalyticsEvent(clickWindow: ClickEvent[]) { '[Amplitude] End Time': new Date(lastClick.timestamp).toISOString(), '[Amplitude] Duration': lastClick.timestamp - firstClick.timestamp, '[Amplitude] Clicks': clickWindow.map((click) => ({ - X: (click.event as MouseEvent).clientX, - Y: (click.event as MouseEvent).clientY, + X: (click.event as MouseEvent).pageX, + Y: (click.event as MouseEvent).pageY, Time: click.timestamp, })), '[Amplitude] Click Count': clickWindow.length, diff --git a/packages/plugin-autocapture-browser/src/pageActions/matchEventToFilter.ts b/packages/plugin-autocapture-browser/src/pageActions/matchEventToFilter.ts index 518efae33..e99842bfa 100644 --- a/packages/plugin-autocapture-browser/src/pageActions/matchEventToFilter.ts +++ b/packages/plugin-autocapture-browser/src/pageActions/matchEventToFilter.ts @@ -1,5 +1,5 @@ import type { Filter } from '@amplitude/analytics-core/lib/esm/types/element-interactions'; -import { ElementBasedTimestampedEvent, ElementBasedEvent } from 'src/helpers'; +import { ElementBasedTimestampedEvent, ElementBasedEvent } from '../helpers'; /** * Matches an event to a single filter diff --git a/packages/plugin-autocapture-browser/src/pageActions/triggers.ts b/packages/plugin-autocapture-browser/src/pageActions/triggers.ts index 7a63e0a9b..3c74f0e65 100644 --- a/packages/plugin-autocapture-browser/src/pageActions/triggers.ts +++ b/packages/plugin-autocapture-browser/src/pageActions/triggers.ts @@ -1,5 +1,5 @@ import type { ElementInteractionsOptions, LabeledEvent, Trigger } from '@amplitude/analytics-core'; -import type { ElementBasedTimestampedEvent, ElementBasedEvent } from 'src/helpers'; +import type { ElementBasedTimestampedEvent, ElementBasedEvent } from '../helpers'; import { matchEventToFilter } from './matchEventToFilter'; import { executeActions } from './actions'; import type { DataExtractor } from '../data-extractor'; 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 4a5ea9671..d0ce0c9b9 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 @@ -18,6 +18,7 @@ describe('trackRageClicks', () => { let allObservables: AllWindowObservables; let shouldTrackRageClick: jest.Mock; let clickObserver: any; + let subscription: ReturnType; beforeEach(() => { jest.useFakeTimers(); @@ -34,20 +35,21 @@ describe('trackRageClicks', () => { [ObservablesEnum.MutationObservable]: new Observable(() => {}), }; shouldTrackRageClick = jest.fn().mockReturnValue(true); + + subscription = trackRageClicks({ + amplitude: mockAmplitude, + allObservables, + shouldTrackRageClick, + }); }); afterEach(() => { + subscription?.unsubscribe(); jest.clearAllMocks(); jest.useRealTimers(); }); it('should track rage clicks when threshold is met', async () => { - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - // Create a mock element const mockElement = document.createElement('div'); @@ -58,8 +60,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime, closestTrackedAncestor: mockElement, @@ -71,8 +73,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime + DEFAULT_RAGE_CLICK_WINDOW_MS + i * 50, // Exceed the time window closestTrackedAncestor: mockElement, @@ -97,16 +99,9 @@ describe('trackRageClicks', () => { }), expect.any(Object), ); - subscription?.unsubscribe(); }); it('should track rage clicks via timer when threshold is met within time window', async () => { - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - // Create a mock element const mockElement = document.createElement('div'); @@ -116,8 +111,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime + i * 50, // Space clicks 50ms apart (well within 1000ms window) closestTrackedAncestor: mockElement, @@ -142,16 +137,9 @@ describe('trackRageClicks', () => { }), expect.any(Object), ); - subscription?.unsubscribe(); }); it('should track if clicks exceed threshold but first click is outside the rage click window', async () => { - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - // Create a mock element const mockElement = document.createElement('div'); @@ -160,8 +148,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime, closestTrackedAncestor: mockElement, @@ -173,8 +161,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime + (DEFAULT_RAGE_CLICK_WINDOW_MS - 200) + i * 50, closestTrackedAncestor: mockElement, @@ -185,16 +173,9 @@ describe('trackRageClicks', () => { await jest.runAllTimersAsync(); expect(mockAmplitude.track).toHaveBeenCalledTimes(1); - subscription?.unsubscribe(); }); it('should not track when clicks are below threshold', () => { - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - // Create a mock element const mockElement = document.createElement('div'); @@ -204,8 +185,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime + i * 50, closestTrackedAncestor: mockElement, @@ -216,16 +197,9 @@ describe('trackRageClicks', () => { jest.advanceTimersByTime(DEFAULT_RAGE_CLICK_WINDOW_MS + 100); expect(mockAmplitude.track).not.toHaveBeenCalled(); - subscription?.unsubscribe(); }); it('should not track when clicks are on different elements', () => { - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - // Create two different mock elements const mockElement1 = document.createElement('div'); const mockElement2 = document.createElement('div'); @@ -236,8 +210,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: i % 2 === 0 ? mockElement1 : mockElement2, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime + i * 50, closestTrackedAncestor: i % 2 === 0 ? mockElement1 : mockElement2, @@ -248,18 +222,11 @@ describe('trackRageClicks', () => { jest.advanceTimersByTime(DEFAULT_RAGE_CLICK_WINDOW_MS + 100); expect(mockAmplitude.track).not.toHaveBeenCalled(); - subscription?.unsubscribe(); }); it('should not track untracked elements', () => { shouldTrackRageClick.mockReturnValue(false); - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - // element that is not tracked const mockElement = document.createElement('div'); @@ -269,8 +236,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime + i * 50, closestTrackedAncestor: mockElement, @@ -281,16 +248,9 @@ describe('trackRageClicks', () => { jest.advanceTimersByTime(DEFAULT_RAGE_CLICK_WINDOW_MS + 100); expect(mockAmplitude.track).not.toHaveBeenCalled(); - subscription?.unsubscribe(); }); it('should handle clicks that exceed the time window correctly', async () => { - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - // Create a mock element const mockElement = document.createElement('div'); @@ -302,8 +262,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime + i * 50, closestTrackedAncestor: mockElement, @@ -317,8 +277,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime + DEFAULT_RAGE_CLICK_WINDOW_MS + 100, closestTrackedAncestor: mockElement, @@ -329,16 +289,9 @@ describe('trackRageClicks', () => { // Should track the first rage click event expect(mockAmplitude.track).toHaveBeenCalledTimes(1); - subscription?.unsubscribe(); }); it('should trigger rage click when switching to different element with enough previous clicks', async () => { - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - // Create two different mock elements const mockElement1 = document.createElement('div'); const mockElement2 = document.createElement('div'); @@ -350,8 +303,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement1, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime + i * 50, closestTrackedAncestor: mockElement1, @@ -365,8 +318,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement2, - clientX: 200, - clientY: 200, + pageX: 200, + pageY: 200, }, timestamp: startTime + DEFAULT_RAGE_CLICK_THRESHOLD * 50 + 100, closestTrackedAncestor: mockElement2, @@ -390,16 +343,9 @@ describe('trackRageClicks', () => { }), expect.any(Object), ); - subscription?.unsubscribe(); }); it('should not trigger rage click when switching to different element without enough previous clicks', () => { - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - // Create two different mock elements const mockElement1 = document.createElement('div'); const mockElement2 = document.createElement('div'); @@ -411,8 +357,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement1, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime + i * 50, closestTrackedAncestor: mockElement1, @@ -424,8 +370,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement2, - clientX: 200, - clientY: 200, + pageX: 200, + pageY: 200, }, timestamp: startTime + (DEFAULT_RAGE_CLICK_THRESHOLD - 1) * 50 + 100, closestTrackedAncestor: mockElement2, @@ -436,62 +382,42 @@ describe('trackRageClicks', () => { // Should NOT track any rage click event expect(mockAmplitude.track).not.toHaveBeenCalled(); - subscription?.unsubscribe(); }); - it('should not track rage clicks when threshold is met but clicks are out of bounds', () => { - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - + it('should not track rage clicks when threshold is met but clicks are out of bounds', async () => { // Create a mock element const mockElement = document.createElement('div'); // Simulate clicks that exceed the time window to trigger immediate rage click detection const startTime = Date.now(); - // First click - clickObserver.next({ - event: { - target: mockElement, - clientX: 100, - clientY: 100, - }, - timestamp: startTime, - closestTrackedAncestor: mockElement, - targetElementProperties: { id: 'test-element' }, - }); - // Add clicks that exceed the time window for (let i = 0; i < DEFAULT_RAGE_CLICK_THRESHOLD; i++) { clickObserver.next({ event: { target: mockElement, - clientX: i === DEFAULT_RAGE_CLICK_THRESHOLD - 1 ? 1000 : 100, - clientY: i === DEFAULT_RAGE_CLICK_THRESHOLD - 1 ? 1000 : 100, + // have the first click and the last n - 1 clicks be in different + // positions to test that the rage click is not triggered + pageX: i === DEFAULT_RAGE_CLICK_THRESHOLD - 1 ? 1000 : 100, + pageY: i === DEFAULT_RAGE_CLICK_THRESHOLD - 1 ? 1000 : 100, + // keep clientX and clientY fixed to confirm it doesn't use + // viewport coordinates any more + clientX: 100, + clientY: 100, }, - timestamp: startTime + DEFAULT_RAGE_CLICK_WINDOW_MS + i * 50, // Exceed the time window + timestamp: startTime + i, closestTrackedAncestor: mockElement, targetElementProperties: { id: 'test-element' }, }); } - // Advance timers for the event to be processed - jest.advanceTimersByTime(DEFAULT_RAGE_CLICK_WINDOW_MS + 100); // Short wait since we're triggering immediate detection + // Run all timers and flush promises + await jest.runAllTimersAsync(); expect(mockAmplitude.track).not.toHaveBeenCalled(); - subscription?.unsubscribe(); }); it('should track rage clicks when threshold is met and next click is out of bounds', async () => { - const subscription = trackRageClicks({ - amplitude: mockAmplitude, - allObservables, - shouldTrackRageClick, - }); - // Create a mock element const mockElement = document.createElement('div'); const startTime = Date.now(); @@ -499,8 +425,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 100, - clientY: 100, + pageX: 100, + pageY: 100, }, timestamp: startTime, closestTrackedAncestor: mockElement, @@ -510,8 +436,8 @@ describe('trackRageClicks', () => { clickObserver.next({ event: { target: mockElement, - clientX: 1000, - clientY: 1000, + pageX: 1000, + pageY: 1000, }, timestamp: startTime, closestTrackedAncestor: mockElement, @@ -519,6 +445,5 @@ describe('trackRageClicks', () => { }); await jest.runAllTimersAsync(); expect(mockAmplitude.track).toHaveBeenCalledTimes(1); - subscription?.unsubscribe(); }); }); diff --git a/test-server/scroll-test.html b/test-server/scroll-test.html new file mode 100644 index 000000000..0e2a94e53 --- /dev/null +++ b/test-server/scroll-test.html @@ -0,0 +1,66 @@ + + + + + + Scroll Test Page + + + +

Scroll Test Page

+

This page has a height of ~15,000px for scroll testing.

+ +
1000px
+
2000px
+
3000px
+
4000px
+
5000px
+
6000px
+
7000px
+
8000px
+
9000px
+
10000px
+
11000px
+
12000px
+
13000px
+
14000px
+
End
+ + + +