Skip to content

Commit efd683c

Browse files
zhu-xiaoweizxkane
authored andcommitted
feat: support batch mode
1 parent b561e00 commit efd683c

File tree

11 files changed

+513
-42
lines changed

11 files changed

+513
-42
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"format": "npx prettier --check 'src/**/*.{js,ts}'",
1111
"lint": "npx eslint src",
1212
"test": "npx jest -w 1 --coverage",
13-
"clean": "rimraf dist"
13+
"clean": "rimraf dist",
14+
"pack": "npm pack"
1415
},
1516
"dependencies": {
1617
"@aws-amplify/core": "^5.5.1",

src/network/NetRequest.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ const logger = new Logger('NetRequest');
1717

1818
export class NetRequest {
1919
static readonly REQUEST_TIMEOUT = 3000;
20+
static readonly BATCH_REQUEST_TIMEOUT = 15000;
2021
static readonly REQUEST_RETRY_TIMES = 3;
22+
static readonly BATCH_REQUEST_RETRY_TIMES = 1;
2123

2224
static async sendRequest(
2325
eventsJson: string,
2426
clickstream: ClickstreamContext,
2527
bundleSequenceId: number,
26-
retryTimes = NetRequest.REQUEST_RETRY_TIMES
28+
retryTimes = NetRequest.REQUEST_RETRY_TIMES,
29+
timeout = NetRequest.REQUEST_TIMEOUT
2730
): Promise<boolean> {
2831
const { configuration, browserInfo } = clickstream;
2932
const queryParams = new URLSearchParams({
@@ -36,10 +39,11 @@ export class NetRequest {
3639
const controller = new AbortController();
3740
const timeoutId = setTimeout(() => {
3841
controller.abort();
39-
}, NetRequest.REQUEST_TIMEOUT);
42+
}, timeout);
4043

4144
const requestOptions: RequestInit = {
4245
method: 'POST',
46+
mode: 'cors',
4347
headers: {
4448
'Content-Type': 'application/json; charset=utf-8',
4549
cookie: configuration.authCookie,

src/provider/ClickstreamProvider.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export class ClickstreamProvider implements AnalyticsProvider {
6060
);
6161
this.eventRecorder = new EventRecorder(this.clickstream);
6262
this.userAttribute = StorageUtil.getUserAttributes();
63+
if (configuration.sendMode === SendMode.Batch) {
64+
this.startTimer();
65+
}
6366
logger.debug(
6467
'Initialize the SDK successfully, configuration is:\n' +
6568
JSON.stringify(this.configuration)
@@ -137,4 +140,15 @@ export class ClickstreamProvider implements AnalyticsProvider {
137140
}
138141
StorageUtil.updateUserAttributes(this.userAttribute);
139142
}
143+
144+
startTimer() {
145+
setInterval(
146+
this.flushEvents.bind(this, this.eventRecorder),
147+
this.configuration.sendEventsInterval
148+
);
149+
}
150+
151+
flushEvents(eventRecorder: EventRecorder) {
152+
eventRecorder.flushEvents();
153+
}
140154
}

src/provider/Event.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,10 @@ export class Event {
221221
PROFILE_SET: '_profile_set',
222222
CLICKSTREAM_ERROR: '_clickstream_error',
223223
};
224+
225+
static readonly Constants = {
226+
PREFIX: '[',
227+
SUFFIX: ']',
228+
LAST_EVENT_IDENTIFIER: '},{"hashCode":',
229+
};
224230
}

src/provider/EventRecorder.ts

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
* and limitations under the License.
1212
*/
1313
import { ConsoleLogger as Logger } from '@aws-amplify/core';
14+
import { LOG_TYPE } from '@aws-amplify/core/lib/Logger';
1415
import { ClickstreamContext } from './ClickstreamContext';
16+
import { Event } from './Event';
1517
import { NetRequest } from '../network/NetRequest';
1618
import { AnalyticsEvent, SendMode } from '../types';
1719
import { StorageUtil } from '../util/StorageUtil';
@@ -21,6 +23,7 @@ const logger = new Logger('EventRecorder');
2123
export class EventRecorder {
2224
clickstream: ClickstreamContext;
2325
bundleSequenceId: number;
26+
isFlushingEvents: boolean;
2427

2528
constructor(clickstream: ClickstreamContext) {
2629
this.clickstream = clickstream;
@@ -29,33 +32,43 @@ export class EventRecorder {
2932

3033
record(event: AnalyticsEvent) {
3134
if (this.clickstream.configuration.isLogEvents) {
32-
logger.level = Logger.LOG_LEVEL.DEBUG;
35+
logger.level = LOG_TYPE.DEBUG;
3336
logger.debug(
3437
`Logged event ${event.event_type}, event attributes:\n
3538
${JSON.stringify(event.attributes)}`
3639
);
3740
}
38-
if (this.clickstream.configuration.sendMode === SendMode.Immediate) {
39-
const eventsJson = JSON.stringify([event]);
40-
NetRequest.sendRequest(
41-
eventsJson,
42-
this.clickstream,
43-
this.bundleSequenceId
44-
).then(result => {
45-
if (result) {
46-
logger.debug('Event send success');
47-
} else {
48-
StorageUtil.saveFailedEvent(event);
41+
switch (this.clickstream.configuration.sendMode) {
42+
case SendMode.Immediate:
43+
this.sendEventImmediate(event);
44+
break;
45+
case SendMode.Batch:
46+
if (!StorageUtil.saveEvent(event)) {
47+
this.sendEventImmediate(event);
4948
}
50-
});
51-
this.plusSequenceId();
5249
}
5350
}
5451

52+
sendEventImmediate(event: AnalyticsEvent) {
53+
const eventsJson = JSON.stringify([event]);
54+
NetRequest.sendRequest(
55+
eventsJson,
56+
this.clickstream,
57+
this.bundleSequenceId
58+
).then(result => {
59+
if (result) {
60+
logger.debug('Event send success');
61+
} else {
62+
StorageUtil.saveFailedEvent(event);
63+
}
64+
});
65+
this.plusSequenceId();
66+
}
67+
5568
sendFailedEvents() {
5669
const failedEvents = StorageUtil.getFailedEvents();
5770
if (failedEvents.length > 0) {
58-
const eventsJson = JSON.stringify(failedEvents);
71+
const eventsJson = failedEvents + Event.Constants.SUFFIX;
5972
NetRequest.sendRequest(
6073
eventsJson,
6174
this.clickstream,
@@ -70,6 +83,66 @@ export class EventRecorder {
7083
}
7184
}
7285

86+
flushEvents() {
87+
if (this.isFlushingEvents) {
88+
return;
89+
}
90+
const [eventsJson, needsFlushTwice] = this.getBatchEvents();
91+
if (eventsJson === '') {
92+
return;
93+
}
94+
this.isFlushingEvents = true;
95+
NetRequest.sendRequest(
96+
eventsJson,
97+
this.clickstream,
98+
this.bundleSequenceId,
99+
NetRequest.BATCH_REQUEST_RETRY_TIMES,
100+
NetRequest.BATCH_REQUEST_TIMEOUT
101+
).then(result => {
102+
if (result) {
103+
StorageUtil.clearEvents(eventsJson);
104+
}
105+
this.isFlushingEvents = false;
106+
if (needsFlushTwice) {
107+
this.flushEvents();
108+
}
109+
});
110+
this.plusSequenceId();
111+
}
112+
113+
getBatchEvents(): [string, boolean] {
114+
let allEventsStr = StorageUtil.getAllEvents();
115+
if (allEventsStr === '') {
116+
return [allEventsStr, false];
117+
} else if (allEventsStr.length <= StorageUtil.MAX_REQUEST_EVENTS_SIZE) {
118+
return [allEventsStr + Event.Constants.SUFFIX, false];
119+
} else {
120+
const isOnlyOneEvent =
121+
allEventsStr.lastIndexOf(Event.Constants.LAST_EVENT_IDENTIFIER) < 0;
122+
const firstEventSize = allEventsStr.indexOf(
123+
Event.Constants.LAST_EVENT_IDENTIFIER
124+
);
125+
if (isOnlyOneEvent) {
126+
return [allEventsStr + Event.Constants.SUFFIX, false];
127+
} else if (firstEventSize > StorageUtil.MAX_REQUEST_EVENTS_SIZE) {
128+
allEventsStr = allEventsStr.substring(0, firstEventSize + 1);
129+
return [allEventsStr + Event.Constants.SUFFIX, true];
130+
} else {
131+
allEventsStr = allEventsStr.substring(
132+
0,
133+
StorageUtil.MAX_REQUEST_EVENTS_SIZE
134+
);
135+
const endIndex = allEventsStr.lastIndexOf(
136+
Event.Constants.LAST_EVENT_IDENTIFIER
137+
);
138+
return [
139+
allEventsStr.substring(0, endIndex + 1) + Event.Constants.SUFFIX,
140+
true,
141+
];
142+
}
143+
}
144+
}
145+
73146
plusSequenceId() {
74147
this.bundleSequenceId += 1;
75148
StorageUtil.saveBundleSequenceId(this.bundleSequenceId);

src/util/StorageUtil.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import { AnalyticsEvent, UserAttribute } from '../types';
1818
const logger = new Logger('StorageUtil');
1919

2020
export class StorageUtil {
21-
static readonly MAX_FAILED_EVENTS_SIZE = 1024 * 512;
21+
static readonly MAX_REQUEST_EVENTS_SIZE = 1024 * 512;
22+
static readonly MAX_FAILED_EVENTS_SIZE = this.MAX_REQUEST_EVENTS_SIZE;
23+
static readonly MAX_BATCH_EVENTS_SIZE = 1024 * 1024;
2224

2325
static readonly prefix = 'aws-solution/clickstream-js/';
2426
static readonly deviceIdKey = this.prefix + 'deviceIdKey';
@@ -28,6 +30,7 @@ export class StorageUtil {
2830
static readonly userFirstTouchTimestampKey =
2931
this.prefix + 'userFirstTouchTimestampKey';
3032
static readonly failedEventsKey = this.prefix + 'failedEventsKey';
33+
static readonly eventsKey = this.prefix + 'eventsKey';
3134

3235
static getDeviceId(): string {
3336
let deviceId = localStorage.getItem(StorageUtil.deviceIdKey) ?? '';
@@ -88,17 +91,19 @@ export class StorageUtil {
8891
return JSON.parse(userAttributes);
8992
}
9093

91-
static getFailedEvents(): AnalyticsEvent[] {
92-
const failedEvents =
93-
localStorage.getItem(StorageUtil.failedEventsKey) ?? '[]';
94-
return JSON.parse(failedEvents);
94+
static getFailedEvents(): string {
95+
return localStorage.getItem(StorageUtil.failedEventsKey) ?? '';
9596
}
9697

9798
static saveFailedEvent(event: AnalyticsEvent) {
9899
const { MAX_FAILED_EVENTS_SIZE } = StorageUtil;
99100
const allEvents = StorageUtil.getFailedEvents();
100-
allEvents.push(event);
101-
const eventsStr = JSON.stringify(allEvents);
101+
let eventsStr = '';
102+
if (allEvents === '') {
103+
eventsStr = Event.Constants.PREFIX + JSON.stringify(event);
104+
} else {
105+
eventsStr = allEvents + ',' + JSON.stringify(event);
106+
}
102107
if (eventsStr.length <= MAX_FAILED_EVENTS_SIZE) {
103108
localStorage.setItem(StorageUtil.failedEventsKey, eventsStr);
104109
} else {
@@ -110,4 +115,40 @@ export class StorageUtil {
110115
static clearFailedEvents() {
111116
localStorage.removeItem(StorageUtil.failedEventsKey);
112117
}
118+
119+
static getAllEvents(): string {
120+
return localStorage.getItem(StorageUtil.eventsKey) ?? '';
121+
}
122+
123+
static saveEvent(event: AnalyticsEvent): boolean {
124+
const { MAX_BATCH_EVENTS_SIZE } = StorageUtil;
125+
const allEvents = StorageUtil.getAllEvents();
126+
let eventsStr = '';
127+
if (allEvents === '') {
128+
eventsStr = Event.Constants.PREFIX + JSON.stringify(event);
129+
} else {
130+
eventsStr = allEvents + ',' + JSON.stringify(event);
131+
}
132+
if (eventsStr.length <= MAX_BATCH_EVENTS_SIZE) {
133+
localStorage.setItem(StorageUtil.eventsKey, eventsStr);
134+
return true;
135+
} else {
136+
const maxSize = MAX_BATCH_EVENTS_SIZE / 1024;
137+
logger.warn(`Events reached max cache size of ${maxSize}kb`);
138+
return false;
139+
}
140+
}
141+
142+
static clearEvents(eventsJson: string) {
143+
const deletedEvents = JSON.parse(eventsJson);
144+
const allEvents = JSON.parse(this.getAllEvents() + Event.Constants.SUFFIX);
145+
if (allEvents.length > deletedEvents.length) {
146+
const leftEvents = allEvents.splice(deletedEvents.length);
147+
let leftEventsStr = JSON.stringify(leftEvents);
148+
leftEventsStr = leftEventsStr.substring(0, leftEventsStr.length - 1);
149+
localStorage.setItem(StorageUtil.eventsKey, leftEventsStr);
150+
} else {
151+
localStorage.removeItem(StorageUtil.eventsKey);
152+
}
153+
}
113154
}

test/network/NetRequest.test.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@ describe('ClickstreamAnalytics test', () => {
2020
let clickstream: ClickstreamContext;
2121
let eventJson: string;
2222
beforeEach(async () => {
23-
fetchMock.post('begin:https://localhost:8080/collect', {
24-
status: 200,
25-
body: [],
26-
});
2723
clickstream = new ClickstreamContext(new BrowserInfo(), {
2824
appId: 'testApp',
2925
endpoint: 'https://localhost:8080/collect',
@@ -42,6 +38,10 @@ describe('ClickstreamAnalytics test', () => {
4238
});
4339

4440
test('test request success', async () => {
41+
fetchMock.post('begin:https://localhost:8080/collect', {
42+
status: 200,
43+
body: [],
44+
});
4545
const result = await NetRequest.sendRequest(eventJson, clickstream, 1);
4646
expect(result).toBeTruthy();
4747
});
@@ -54,4 +54,39 @@ describe('ClickstreamAnalytics test', () => {
5454
const result = await NetRequest.sendRequest(eventJson, clickstream, 1);
5555
expect(result).toBeFalsy();
5656
});
57+
58+
test('test request fail with code 404', async () => {
59+
fetchMock.post('begin:https://localhost:8080/collectFail', 404);
60+
clickstream = new ClickstreamContext(new BrowserInfo(), {
61+
appId: 'testApp',
62+
endpoint: 'https://localhost:8080/collectFail',
63+
});
64+
const result = await NetRequest.sendRequest(eventJson, clickstream, 1);
65+
expect(result).toBeFalsy();
66+
});
67+
68+
test('test request timeout', async () => {
69+
fetchMock.post(
70+
'begin:https://localhost:8080/collect',
71+
{
72+
status: 200,
73+
body: [],
74+
},
75+
{
76+
delay: 1000,
77+
}
78+
);
79+
clickstream = new ClickstreamContext(new BrowserInfo(), {
80+
appId: 'testApp',
81+
endpoint: 'https://localhost:8080/collect',
82+
});
83+
const result = await NetRequest.sendRequest(
84+
eventJson,
85+
clickstream,
86+
1,
87+
1,
88+
200
89+
);
90+
expect(result).toBeFalsy();
91+
});
5792
});

0 commit comments

Comments
 (0)