Skip to content

Commit b0ca13d

Browse files
zhu-xiaoweixiaoweii
andauthored
feat: add preset event _page_load (#40)
Co-authored-by: xiaoweii <xiaoweii@amazom.com>
1 parent 95ea1ec commit b0ca13d

14 files changed

+380
-6
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ ClickstreamAnalytics.init({
156156
isTrackClickEvents: true,
157157
isTrackSearchEvents: true,
158158
isTrackScrollEvents: true,
159+
isTrackPageLoadEvents: true,
159160
pageType: PageType.SPA,
160161
isLogEvents: false,
161162
authCookie: "your auth cookie",
@@ -170,13 +171,14 @@ Here is an explanation of each property:
170171
- **appId (Required)**: the app id of your project in control plane.
171172
- **endpoint (Required)**: the endpoint path you will upload the event to AWS server.
172173
- **sendMode**: EventMode.Immediate, EventMode.Batch, default is Immediate mode.
173-
- **sendEventsInterval**: event sending interval millisecond, works only bath send mode, the default value is `5000`
174+
- **sendEventsInterval**: event sending interval millisecond, works only bath send mode, the default value is `5000`
174175
- **isTrackPageViewEvents**: whether auto record page view events in browser, default is `true`
175176
- **isTrackUserEngagementEvents**: whether auto record user engagement events in browser, default is `true`
176177
- **isTrackClickEvents**: whether auto record link click events in browser, default is `true`
177178
- **isTrackSearchEvents**: whether auto record search result page events in browser, default is `true`
178179
- **isTrackScrollEvents**: whether auto record page scroll events in browser, default is `true`
179-
- **pageType**: the website type, `SPA` for single page application, `multiPageApp` for multiple page application, default is `SPA`. This attribute works only when the attribute `isTrackPageViewEvents`'s value is `true`.
180+
- **isTrackPageLoadEvents**: whether auto record page load performance events in browser, default is `false`
181+
- **pageType**: the website type, `SPA` for single page application, `multiPageApp` for multiple page application, default is `SPA`. This attribute works only when the attribute `isTrackPageViewEvents`'s value is `true`
180182
- **isLogEvents**: whether to print out event json for debugging, default is false.
181183
- **authCookie**: your auth cookie for AWS application load balancer auth cookie.
182184
- **sessionTimeoutDuration**: the duration for session timeout millisecond, default is 1800000
@@ -197,6 +199,7 @@ ClickstreamAnalytics.updateConfigure({
197199
isTrackClickEvents: false,
198200
isTrackScrollEvents: false,
199201
isTrackSearchEvents: false,
202+
isTrackPageLoadEvents: false,
200203
});
201204
```
202205

jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ module.exports = {
1515
testMatch: ['**/*.test.ts'],
1616
moduleFileExtensions: ['ts', 'js'],
1717
testEnvironment: 'jsdom',
18+
coveragePathIgnorePatterns: ['test'],
1819
};

src/provider/ClickstreamProvider.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { EventRecorder } from './EventRecorder';
2020
import { BrowserInfo } from '../browser';
2121
import { PageViewTracker, SessionTracker } from '../tracker';
2222
import { ClickTracker } from '../tracker/ClickTracker';
23+
import { PageLoadTracker } from '../tracker/PageLoadTracker';
2324
import { ScrollTracker } from '../tracker/ScrollTracker';
2425
import {
2526
AnalyticsEvent,
@@ -47,6 +48,7 @@ export class ClickstreamProvider implements AnalyticsProvider {
4748
pageViewTracker: PageViewTracker;
4849
clickTracker: ClickTracker;
4950
scrollTracker: ScrollTracker;
51+
pageLoadTracker: PageLoadTracker;
5052

5153
constructor() {
5254
this.configuration = {
@@ -59,6 +61,7 @@ export class ClickstreamProvider implements AnalyticsProvider {
5961
isTrackClickEvents: true,
6062
isTrackSearchEvents: true,
6163
isTrackScrollEvents: true,
64+
isTrackPageLoadEvents: false,
6265
pageType: PageType.SPA,
6366
isLogEvents: false,
6467
sessionTimeoutDuration: 1800000,
@@ -86,10 +89,12 @@ export class ClickstreamProvider implements AnalyticsProvider {
8689
this.pageViewTracker = new PageViewTracker(this, this.context);
8790
this.clickTracker = new ClickTracker(this, this.context);
8891
this.scrollTracker = new ScrollTracker(this, this.context);
92+
this.pageLoadTracker = new PageLoadTracker(this, this.context);
8993
this.sessionTracker.setUp();
9094
this.pageViewTracker.setUp();
9195
this.clickTracker.setUp();
9296
this.scrollTracker.setUp();
97+
this.pageLoadTracker.setUp();
9398
if (configuration.sendMode === SendMode.Batch) {
9499
this.startTimer();
95100
}

src/provider/Event.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,43 @@ export class Event {
7070
OUTBOUND: '_outbound',
7171
SEARCH_KEY: '_search_key',
7272
SEARCH_TERM: '_search_term',
73+
TIMING_ATTRIBUTES: [
74+
'duration',
75+
'deliveryType',
76+
'nextHopProtocol',
77+
'renderBlockingStatus',
78+
'startTime',
79+
'redirectStart',
80+
'redirectEnd',
81+
'workerStart',
82+
'fetchStart',
83+
'domainLookupStart',
84+
'domainLookupEnd',
85+
'connectStart',
86+
'secureConnectionStart',
87+
'connectEnd',
88+
'requestStart',
89+
'firstInterimResponseStart',
90+
'responseStart',
91+
'responseEnd',
92+
'transferSize',
93+
'encodedBodySize',
94+
'decodedBodySize',
95+
'responseStatus',
96+
'unloadEventStart',
97+
'unloadEventEnd',
98+
'domInteractive',
99+
'domContentLoadedEventStart',
100+
'domContentLoadedEventEnd',
101+
'domComplete',
102+
'loadEventStart',
103+
'loadEventEnd',
104+
'type',
105+
'redirectCount',
106+
'activationStart',
107+
'criticalCHRestart',
108+
'serverTiming',
109+
],
73110
};
74111

75112
static readonly PresetEvent = {
@@ -84,6 +121,7 @@ export class Event {
84121
CLICK: '_click',
85122
SEARCH: '_search',
86123
SCROLL: '_scroll',
124+
PAGE_LOAD: '_page_load',
87125
};
88126

89127
static readonly Constants = {

src/provider/EventRecorder.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,7 @@ export class EventRecorder {
3535
record(event: AnalyticsEvent, isImmediate = false) {
3636
if (this.context.configuration.isLogEvents) {
3737
logger.level = LOG_TYPE.DEBUG;
38-
logger.debug(
39-
`Logged event ${event.event_type}, event attributes:\n
40-
${JSON.stringify(event)}`
41-
);
38+
logger.debug(`Logged event ${event.event_type}\n`, event);
4239
}
4340
const currentMode = this.context.configuration.sendMode;
4441
if (currentMode === SendMode.Immediate || isImmediate) {

src/tracker/PageLoadTracker.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
5+
* with the License. A copy of the License is located at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
10+
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
11+
* and limitations under the License.
12+
*/
13+
14+
import { BaseTracker } from './BaseTracker';
15+
import { Event } from '../provider';
16+
import { ClickstreamAttribute } from '../types';
17+
18+
export class PageLoadTracker extends BaseTracker {
19+
observer: PerformanceObserver;
20+
21+
init() {
22+
this.trackPageLoad = this.trackPageLoad.bind(this);
23+
if (this.isSupportedEnv()) {
24+
this.observer = new PerformanceObserver(() => {
25+
this.trackPageLoad();
26+
});
27+
this.observer.observe({ entryTypes: ['navigation'] });
28+
}
29+
if (this.isPageLoaded()) {
30+
this.trackPageLoad();
31+
}
32+
}
33+
34+
trackPageLoad() {
35+
if (!this.context.configuration.isTrackPageLoadEvents) return;
36+
const performanceEntries = performance.getEntriesByType('navigation');
37+
if (performanceEntries && performanceEntries.length > 0) {
38+
const latestPerformance =
39+
performanceEntries[performanceEntries.length - 1];
40+
const eventAttributes: ClickstreamAttribute = {};
41+
for (const key in latestPerformance) {
42+
const value = (latestPerformance as any)[key];
43+
const valueType = typeof value;
44+
if (Event.ReservedAttribute.TIMING_ATTRIBUTES.includes(key)) {
45+
if (valueType === 'string' || valueType === 'number') {
46+
eventAttributes[key] = value;
47+
} else if (Array.isArray(value) && value.length > 0) {
48+
eventAttributes[key] = JSON.stringify(value);
49+
}
50+
}
51+
}
52+
this.provider.record({
53+
name: Event.PresetEvent.PAGE_LOAD,
54+
attributes: eventAttributes,
55+
});
56+
}
57+
}
58+
59+
isPageLoaded() {
60+
const performanceEntries = performance.getEntriesByType('navigation');
61+
return performanceEntries?.[0]?.duration > 0 || false;
62+
}
63+
64+
isSupportedEnv(): boolean {
65+
return !!performance && !!PerformanceObserver;
66+
}
67+
}

src/types/Analytics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export interface Configuration {
3131
isTrackClickEvents?: boolean;
3232
isTrackScrollEvents?: boolean;
3333
isTrackSearchEvents?: boolean;
34+
isTrackPageLoadEvents?: boolean;
3435
}
3536

3637
export enum SendMode {

test/ClickstreamAnalytics.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
1111
* and limitations under the License.
1212
*/
13+
import { setUpBrowserPerformance } from "./browser/BrowserUtil";
1314
import { ClickstreamAnalytics, Item, SendMode } from '../src';
1415
import { NetRequest } from '../src/network/NetRequest';
1516
import { Event } from '../src/provider';
@@ -22,6 +23,7 @@ describe('ClickstreamAnalytics test', () => {
2223
jest
2324
.spyOn(NetRequest, 'sendRequest')
2425
.mockImplementation(mockSendRequestSuccess);
26+
setUpBrowserPerformance();
2527
});
2628

2729
afterEach(() => {

test/browser/BrowserUtil.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
5+
* with the License. A copy of the License is located at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
10+
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
11+
* and limitations under the License.
12+
*/
13+
import { MockObserver } from './MockObserver';
14+
15+
export function setUpBrowserPerformance() {
16+
(global as any).PerformanceObserver = MockObserver;
17+
setPerformanceEntries(false);
18+
}
19+
20+
export function setPerformanceEntries(isLoaded = true) {
21+
Object.defineProperty(window, 'performance', {
22+
writable: true,
23+
value: {
24+
getEntriesByType: jest
25+
.fn()
26+
.mockImplementation(
27+
isLoaded ? getEntriesByType : getEntriesByTypeUnload
28+
),
29+
},
30+
});
31+
}
32+
33+
function getEntriesByType(): PerformanceEntryList {
34+
return <PerformanceEntry[]>(<unknown>[
35+
{
36+
name: 'https://aws.amazon.com/cn/',
37+
entryType: 'navigation',
38+
startTime: 0,
39+
duration: 3444.4000000059605,
40+
initiatorType: 'navigation',
41+
deliveryType: 'indirect',
42+
nextHopProtocol: 'h2',
43+
renderBlockingStatus: 'non-blocking',
44+
workerStart: 0,
45+
redirectStart: 2,
46+
redirectEnd: 2.2,
47+
fetchStart: 2.2000000178813934,
48+
domainLookupStart: 2.2000000178813934,
49+
domainLookupEnd: 2.2000000178813934,
50+
connectStart: 2.2000000178813934,
51+
secureConnectionStart: 2.2000000178813934,
52+
connectEnd: 2.2000000178813934,
53+
requestStart: 745.9000000059605,
54+
responseStart: 1006.7000000178814,
55+
firstInterimResponseStart: 0,
56+
responseEnd: 1321.300000011921,
57+
transferSize: 167553,
58+
encodedBodySize: 167253,
59+
decodedBodySize: 1922019,
60+
responseStatus: 200,
61+
serverTiming: [
62+
{
63+
name: 'cache',
64+
duration: 0,
65+
description: 'hit-front',
66+
},
67+
{
68+
name: 'host',
69+
duration: 0,
70+
description: 'cp3062',
71+
},
72+
],
73+
unloadEventStart: 1011.9000000059605,
74+
unloadEventEnd: 1011.9000000059605,
75+
domInteractive: 1710.9000000059605,
76+
domContentLoadedEventStart: 1712.7000000178814,
77+
domContentLoadedEventEnd: 1714.7000000178814,
78+
domComplete: 3440.4000000059605,
79+
loadEventStart: 3444.2000000178814,
80+
loadEventEnd: 3444.4000000059605,
81+
type: 'reload',
82+
redirectCount: 0,
83+
activationStart: 0,
84+
criticalCHRestart: 0,
85+
},
86+
]);
87+
}
88+
89+
function getEntriesByTypeUnload(): PerformanceEntryList {
90+
return <PerformanceEntry[]>(<unknown>[
91+
{
92+
name: 'https://aws.amazon.com/cn/',
93+
entryType: 'navigation',
94+
startTime: 0,
95+
duration: 0,
96+
},
97+
]);
98+
}

test/browser/MockObserver.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance
5+
* with the License. A copy of the License is located at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES
10+
* OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions
11+
* and limitations under the License.
12+
*/
13+
export class MockObserver {
14+
private readonly callback: () => void;
15+
16+
constructor(callback: () => void) {
17+
this.callback = callback;
18+
}
19+
20+
observe(options: any) {
21+
console.log(options);
22+
}
23+
24+
call() {
25+
this.callback();
26+
}
27+
}

0 commit comments

Comments
 (0)