Skip to content

Commit 11f1240

Browse files
zhu-xiaoweixiaoweii
andauthored
feat: exclude idle time in user engagement duration (#52)
Co-authored-by: xiaoweii <xiaoweii@amazom.com>
1 parent 3f23ec3 commit 11f1240

File tree

8 files changed

+101
-18
lines changed

8 files changed

+101
-18
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ ClickstreamAnalytics.init({
176176
isLogEvents: false,
177177
authCookie: "your auth cookie",
178178
sessionTimeoutDuration: 1800000,
179+
idleTimeoutDuration: 120000,
179180
searchKeyWords: ['product', 'class'],
180181
domainList: ['example1.com', 'example2.com'],
181182
});
@@ -199,6 +200,7 @@ Here is an explanation of each property:
199200
- **isLogEvents**: whether to print out event json for debugging, default is false.
200201
- **authCookie**: your auth cookie for AWS application load balancer auth cookie.
201202
- **sessionTimeoutDuration**: the duration for session timeout millisecond, default is 1800000
203+
- **idleTimeoutDuration**: the maximum duration of user inactivity before triggering an idle state, default is 120000 millisecond, Any idle duration exceeding this threshold will be removed in the user_engagement events on the current page.
202204
- **searchKeyWords**: the customized Keywords for trigger the `_search` event, by default we detect `q`, `s`, `search`, `query` and `keyword` in query parameters.
203205
- **domainList**: if your website cross multiple domain, you can customize the domain list. The `_outbound` attribute of the `_click` event will be true when a link leads to a website that's not a part of your configured domain.
204206

@@ -218,7 +220,7 @@ ClickstreamAnalytics.updateConfigure({
218220
isTrackSearchEvents: false,
219221
isTrackPageLoadEvents: false,
220222
isTrackAppStartEvents: true,
221-
isTrackAppStartEvents: true,
223+
isTrackAppEndEvents: true,
222224
});
223225
```
224226

src/provider/ClickstreamProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export class ClickstreamProvider implements AnalyticsProvider {
6767
pageType: PageType.SPA,
6868
isLogEvents: false,
6969
sessionTimeoutDuration: 1800000,
70+
idleTimeoutDuration: 120000,
7071
searchKeyWords: [],
7172
domainList: [],
7273
globalAttributes: {},

src/tracker/ClickTracker.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import { Logger } from '@aws-amplify/core';
1515
import { BaseTracker } from './BaseTracker';
16+
import { PageViewTracker } from './PageViewTracker';
1617
import { Event } from '../provider';
1718

1819
const logger = new Logger('ClickTracker');
@@ -33,6 +34,7 @@ export class ClickTracker extends BaseTracker {
3334
}
3435

3536
trackDocumentClick(event: MouseEvent) {
37+
PageViewTracker.updateIdleDuration();
3638
if (!this.context.configuration.isTrackClickEvents) return;
3739
const targetElement = event.target as Element;
3840
const element = this.findATag(targetElement);

src/tracker/PageViewTracker.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,14 @@ export class PageViewTracker extends BaseTracker {
2626
lastEngageTime = 0;
2727
lastScreenStartTimestamp = 0;
2828
isFirstTime = true;
29+
static lastActiveTimestamp = 0;
30+
static idleDuration = 0;
31+
private static idleTimeoutDuration = 0;
2932

3033
init() {
34+
PageViewTracker.lastActiveTimestamp = new Date().getTime();
35+
PageViewTracker.idleTimeoutDuration =
36+
this.provider.configuration.idleTimeoutDuration;
3137
const configuredSearchKeywords = this.provider.configuration.searchKeyWords;
3238
Object.assign(this.searchKeywords, configuredSearchKeywords);
3339
this.onPageChange = this.onPageChange.bind(this);
@@ -51,6 +57,7 @@ export class PageViewTracker extends BaseTracker {
5157
}
5258

5359
onPageChange() {
60+
PageViewTracker.updateIdleDuration();
5461
if (this.context.configuration.isTrackPageViewEvents) {
5562
const previousPageUrl = StorageUtil.getPreviousPageUrl();
5663
const previousPageTitle = StorageUtil.getPreviousPageTitle();
@@ -114,6 +121,8 @@ export class PageViewTracker extends BaseTracker {
114121

115122
updateLastScreenStartTimestamp() {
116123
this.lastScreenStartTimestamp = new Date().getTime();
124+
PageViewTracker.idleDuration = 0;
125+
PageViewTracker.lastActiveTimestamp = this.lastScreenStartTimestamp;
117126
}
118127

119128
recordUserEngagement(isImmediate = false) {
@@ -133,7 +142,10 @@ export class PageViewTracker extends BaseTracker {
133142
}
134143

135144
getLastEngageTime() {
136-
return new Date().getTime() - this.lastScreenStartTimestamp;
145+
const duration = new Date().getTime() - this.lastScreenStartTimestamp;
146+
const engageTime = duration - PageViewTracker.idleDuration;
147+
PageViewTracker.idleDuration = 0;
148+
return engageTime;
137149
}
138150

139151
isMultiPageApp() {
@@ -159,8 +171,17 @@ export class PageViewTracker extends BaseTracker {
159171
}
160172
}
161173
}
174+
175+
static updateIdleDuration() {
176+
const currentTimestamp = new Date().getTime();
177+
const idleDuration = currentTimestamp - PageViewTracker.lastActiveTimestamp;
178+
if (idleDuration > PageViewTracker.idleTimeoutDuration) {
179+
PageViewTracker.idleDuration += idleDuration;
180+
}
181+
PageViewTracker.lastActiveTimestamp = currentTimestamp;
182+
}
162183
}
163184

164-
enum Constants {
185+
export enum Constants {
165186
minEngagementTime = 1000,
166187
}

src/tracker/ScrollTracker.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
*/
1313

1414
import { BaseTracker } from './BaseTracker';
15+
import { PageViewTracker } from './PageViewTracker';
1516
import { Event } from '../provider';
1617
import { StorageUtil } from '../util/StorageUtil';
1718

@@ -20,7 +21,14 @@ export class ScrollTracker extends BaseTracker {
2021

2122
init() {
2223
this.trackScroll = this.trackScroll.bind(this);
23-
document.addEventListener('scroll', this.trackScroll);
24+
const throttledTrackScroll = this.throttle(this.trackScroll, 100);
25+
document.addEventListener('scroll', throttledTrackScroll, {
26+
passive: true,
27+
});
28+
const throttledMouseMove = this.throttle(this.onMouseMove, 100);
29+
document.addEventListener('mousemove', throttledMouseMove, {
30+
passive: true,
31+
});
2432
this.isFirstTime = true;
2533
}
2634

@@ -29,6 +37,7 @@ export class ScrollTracker extends BaseTracker {
2937
}
3038

3139
trackScroll() {
40+
PageViewTracker.updateIdleDuration();
3241
if (!this.context.configuration.isTrackScrollEvents) return;
3342
const scrollY = window.scrollY || document.documentElement.scrollTop;
3443
const ninetyPercentHeight = document.body.scrollHeight * 0.9;
@@ -45,4 +54,20 @@ export class ScrollTracker extends BaseTracker {
4554
this.isFirstTime = false;
4655
}
4756
}
57+
58+
onMouseMove() {
59+
PageViewTracker.updateIdleDuration();
60+
}
61+
62+
throttle(func: (...args: any[]) => void, delay: number) {
63+
let timeout: ReturnType<typeof setTimeout> | null = null;
64+
return function (this: any, ...args: any[]) {
65+
if (!timeout) {
66+
timeout = setTimeout(() => {
67+
func.apply(this, args);
68+
timeout = null;
69+
}, delay);
70+
}
71+
};
72+
}
4873
}

src/types/Analytics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface ClickstreamConfiguration extends Configuration {
1818
readonly sendEventsInterval?: number;
1919
readonly pageType?: PageType;
2020
readonly sessionTimeoutDuration?: number;
21+
readonly idleTimeoutDuration?: number;
2122
readonly searchKeyWords?: string[];
2223
readonly domainList?: string[];
2324
readonly globalAttributes?: ClickstreamAttribute;

test/tracker/PageViewTracker.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,18 @@ describe('PageViewTracker test', () => {
317317
expect(recordMethodMock).toBeCalled();
318318
});
319319

320+
test('test get engagement time with idle time', async () => {
321+
pageViewTracker.lastScreenStartTimestamp = new Date().getTime();
322+
(provider.configuration as any).idleTimeoutDuration = 100;
323+
pageViewTracker.setUp();
324+
await sleep(110);
325+
PageViewTracker.updateIdleDuration();
326+
await sleep(100);
327+
expect(PageViewTracker.idleDuration > 0).toBeTruthy();
328+
const lastEngageTime = pageViewTracker.getLastEngageTime();
329+
expect(lastEngageTime < 150).toBeTruthy();
330+
});
331+
320332
function openPageA() {
321333
Object.defineProperty(window.document, 'title', {
322334
writable: true,

test/tracker/ScrollTracker.test.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import {
1717
ClickstreamProvider,
1818
EventRecorder,
1919
} from '../../src/provider';
20-
import { Session, SessionTracker } from '../../src/tracker';
20+
import { Session, SessionTracker, PageViewTracker } from '../../src/tracker';
2121
import { ScrollTracker } from '../../src/tracker/ScrollTracker';
22-
import { StorageUtil } from "../../src/util/StorageUtil";
22+
import { StorageUtil } from '../../src/util/StorageUtil';
2323

2424
describe('ScrollTracker test', () => {
2525
let provider: ClickstreamProvider;
@@ -28,7 +28,7 @@ describe('ScrollTracker test', () => {
2828
let recordMethodMock: any;
2929

3030
beforeEach(() => {
31-
StorageUtil.clearAll()
31+
StorageUtil.clearAll();
3232
provider = new ClickstreamProvider();
3333

3434
Object.assign(provider.configuration, {
@@ -63,27 +63,29 @@ describe('ScrollTracker test', () => {
6363
expect(scrollTracker.isFirstTime).toBeTruthy();
6464
});
6565

66-
test('test scroll for not reach ninety percent', () => {
66+
test('test scroll for not reach ninety percent', async () => {
6767
const trackScrollMock = jest.spyOn(scrollTracker, 'trackScroll');
6868
scrollTracker.setUp();
6969
setScrollHeight(2000);
7070
(window as any).innerHeight = 1000;
7171
setScrollY(100);
7272
window.document.dispatchEvent(new window.Event('scroll'));
73+
await sleep(110);
7374
expect(recordMethodMock).not.toBeCalled();
7475
expect(trackScrollMock).toBeCalled();
7576
});
7677

77-
test('test scroll for reached ninety percent', () => {
78+
test('test scroll for reached ninety percent', async () => {
7879
const trackScrollMock = jest.spyOn(scrollTracker, 'trackScroll');
7980
scrollTracker.setUp();
80-
performScrollToBottom();
81+
await performScrollToBottom();
82+
await sleep(110);
8183
expect(recordMethodMock).toBeCalled();
8284
expect(trackScrollMock).toBeCalled();
8385
expect(scrollTracker.isFirstTime).toBeFalsy();
8486
});
8587

86-
test('test window scroll for reached ninety percent using scrollTop api', () => {
88+
test('test window scroll for reached ninety percent using scrollTop api', async () => {
8789
const trackScrollMock = jest.spyOn(scrollTracker, 'trackScroll');
8890
scrollTracker.setUp();
8991
Object.defineProperty(window, 'scrollY', {
@@ -97,46 +99,59 @@ describe('ScrollTracker test', () => {
9799
value: 150,
98100
});
99101
window.document.dispatchEvent(new window.Event('scroll'));
102+
await sleep(110);
100103
expect(trackScrollMock).toBeCalled();
101104
expect(recordMethodMock).toBeCalled();
102105
});
103106

104-
test('test scroll for reached ninety percent and scroll event is disabled', () => {
107+
test('test scroll for reached ninety percent and scroll event is disabled', async () => {
105108
const trackScrollMock = jest.spyOn(scrollTracker, 'trackScroll');
106109
provider.configuration.isTrackScrollEvents = false;
107110
scrollTracker.setUp();
108-
performScrollToBottom();
111+
await performScrollToBottom();
109112
expect(trackScrollMock).toBeCalled();
110113
expect(recordMethodMock).not.toBeCalled();
111114
});
112115

113-
test('test scroll for reached ninety percent twice', () => {
116+
test('test scroll for reached ninety percent twice', async () => {
114117
const trackScrollMock = jest.spyOn(scrollTracker, 'trackScroll');
115118
scrollTracker.setUp();
116-
performScrollToBottom();
119+
await performScrollToBottom();
117120
window.document.dispatchEvent(new window.Event('scroll'));
121+
await sleep(110);
118122
expect(recordMethodMock).toBeCalledTimes(1);
119123
expect(trackScrollMock).toBeCalledTimes(2);
120124
expect(scrollTracker.isFirstTime).toBeFalsy();
121125
});
122126

123-
test('test scroll for enter new page', () => {
127+
test('test scroll for enter new page', async () => {
124128
const trackScrollMock = jest.spyOn(scrollTracker, 'trackScroll');
125129
scrollTracker.setUp();
126-
performScrollToBottom();
130+
await performScrollToBottom();
127131
scrollTracker.enterNewPage();
128132
expect(scrollTracker.isFirstTime).toBeTruthy();
129133
window.document.dispatchEvent(new window.Event('scroll'));
134+
await sleep(110);
130135
expect(recordMethodMock).toBeCalledTimes(2);
131136
expect(trackScrollMock).toBeCalledTimes(2);
132137
expect(scrollTracker.isFirstTime).toBeFalsy();
133138
});
134139

135-
function performScrollToBottom() {
140+
test('test mouse move will update the idle duration', async () => {
141+
const onMouseMoveMock = jest.spyOn(scrollTracker, 'onMouseMove');
142+
scrollTracker.setUp();
143+
window.document.dispatchEvent(new window.Event('mousemove'));
144+
await sleep(110);
145+
expect(onMouseMoveMock).toBeCalled();
146+
expect(PageViewTracker.lastActiveTimestamp > 0).toBeTruthy();
147+
});
148+
149+
async function performScrollToBottom() {
136150
setScrollHeight(1000);
137151
(window as any).innerHeight = 800;
138152
setScrollY(150);
139153
window.document.dispatchEvent(new window.Event('scroll'));
154+
await sleep(110);
140155
}
141156

142157
function setScrollHeight(height: number) {
@@ -152,4 +167,8 @@ describe('ScrollTracker test', () => {
152167
value: height,
153168
});
154169
}
170+
171+
function sleep(ms: number): Promise<void> {
172+
return new Promise(resolve => setTimeout(resolve, ms));
173+
}
155174
});

0 commit comments

Comments
 (0)