Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/plugin-autocapture-browser/src/autocapture-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export enum ObservablesEnum {
// ErrorObservable = 'errorObservable',
NavigateObservable = 'navigateObservable',
MutationObservable = 'mutationObservable',
VisibilityChangeObservable = 'visibilityChangeObservable',
}

export interface AllWindowObservables {
Expand All @@ -58,6 +59,7 @@ export interface AllWindowObservables {
// [ObservablesEnum.ErrorObservable]: Observable<TimestampedEvent<ErrorEvent>>;
[ObservablesEnum.NavigateObservable]: Observable<TimestampedEvent<NavigateEvent>> | undefined;
[ObservablesEnum.MutationObservable]: Observable<TimestampedEvent<MutationRecord[]>>;
[ObservablesEnum.VisibilityChangeObservable]?: Observable<TimestampedEvent<Event>>;
}

export const autocapturePlugin = (options: ElementInteractionsOptions = {}): BrowserEnrichmentPlugin => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function trackDeadClick({
getEventProperties: (actionType: ActionType, element: Element) => Record<string, any>;
shouldTrackDeadClick: shouldTrackEvent;
}) {
const { clickObservable, mutationObservable, navigateObservable } = allObservables;
const { clickObservable, mutationObservable, navigateObservable, visibilityChangeObservable } = allObservables;

const filteredClickObservable = clickObservable.pipe(
filter(filterOutNonTrackableEvents),
Expand All @@ -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(
Expand Down
12 changes: 11 additions & 1 deletion packages/plugin-autocapture-browser/src/frustration-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -63,6 +63,15 @@ export const frustrationPlugin = (options: FrustrationInteractionsOptions = {}):
);
}

const visibilityChangeObservable = fromEvent<TimestampedEvent<Event>>(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) =>
Expand All @@ -76,6 +85,7 @@ export const frustrationPlugin = (options: FrustrationInteractionsOptions = {}):
[ObservablesEnum.ChangeObservable]: new Observable<ElementBasedTimestampedEvent<Event>>(), // Empty observable since we don't need change events
[ObservablesEnum.NavigateObservable]: navigateObservable,
[ObservablesEnum.MutationObservable]: enrichedMutationObservable,
[ObservablesEnum.VisibilityChangeObservable]: visibilityChangeObservable,
};
};

Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-autocapture-browser/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ export type AutoCaptureOptionsWithDefaults = Required<
export type BaseTimestampedEvent<T> = {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ describe('trackDeadClick', () => {
let clickObservable: Subject<any>;
let mutationObservable: Subject<any>;
let navigateObservable: Subject<any>;
let visibilityChangeObservable: Subject<any>;
let allObservables: AllWindowObservables;
let shouldTrackDeadClick: jest.Mock;
let getEventProperties: jest.Mock;
Expand All @@ -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' });
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
5 changes: 5 additions & 0 deletions test-server/autocapture/element-interactions.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ <h2>Clickable Elements Reference</h2>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://amplitude.com" class="role-link" target="_blank">New Page (no dead click)</a></td>
<td class="center">Yes</td>
<td>Always, unless <code>tabindex="-1"</code> or <code>disabled</code></td>
</tr>
<tr>
<td><a href="#" class="role-link">Link</a></td>
<td class="center">Yes</td>
Expand Down
Loading