Skip to content

Commit 90b9e9e

Browse files
zhu-xiaoweixiaoweii
andauthored
feat: support sending events in background (#10)
Co-authored-by: xiaoweii <xiaoweii@amazom.com>
1 parent 6b3b527 commit 90b9e9e

18 files changed

+413
-59
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ npm install @aws/clickstream-web
1515
```
1616

1717
### Initialize the SDK
18-
You need to configure the SDK with default information before using it. Copy your configuration code from your clickstream solution control plane, the configuration code should look like as follows. You can also manually add this code snippet and replace the values of appId and endpoint after you registered app to a data pipeline in the Clickstream Analytics solution console.
18+
Copy your configuration code from your clickstream solution web console, we recommended you add the code to your app's root entry point, for example `index.js/app.tsx` in React or `main.ts` in Vue/Angular, the configuration code should look like as follows. You can also manually add this code snippet and replace the values of appId and endpoint after you registered app to a data pipeline in the Clickstream Analytics solution console.
1919

2020
```typescript
2121
import { ClickstreamAnalytics } from '@aws/clickstream-web';
@@ -87,6 +87,20 @@ ClickstreamAnalytics.record({
8787
});
8888
```
8989

90+
#### Send event immediate in batch mode
91+
92+
When you are in batch mode, you can still send an event immediately by setting the `isImmediate` attribute, as in the following code:
93+
94+
```typescript
95+
import { ClickstreamAnalytics } from '@aws/clickstream-web';
96+
97+
ClickstreamAnalytics.record({
98+
name: 'immediateEvent',
99+
attributes: { url: 'https://example.com' },
100+
isImmediate: true,
101+
});
102+
```
103+
90104
#### Other configurations
91105
In addition to the required `appId` and `endpoint`, you can configure other information to get more customized usage:
92106

package-lock.json

Lines changed: 16 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/browser/BrowserInfo.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ export class BrowserInfo {
6161
);
6262
}
6363

64+
static isFirefox(): boolean {
65+
return navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
66+
}
67+
68+
static isNetworkOnLine(): boolean {
69+
return navigator.onLine;
70+
}
71+
6472
static getCurrentPageUrl(): string {
6573
if (!BrowserInfo.isBrowser()) return '';
6674
else return window.location.href;

src/network/NetRequest.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export class NetRequest {
2020
static readonly BATCH_REQUEST_TIMEOUT = 15000;
2121
static readonly REQUEST_RETRY_TIMES = 3;
2222
static readonly BATCH_REQUEST_RETRY_TIMES = 1;
23+
static readonly KEEP_ALIVE_SIZE_LIMIT = 64 * 1024;
2324

2425
static async sendRequest(
2526
eventsJson: string,
@@ -40,7 +41,8 @@ export class NetRequest {
4041
const timeoutId = setTimeout(() => {
4142
controller.abort();
4243
}, timeout);
43-
44+
const inputSizeInBytes = new Blob([eventsJson]).size;
45+
const isKeepAlive = inputSizeInBytes < NetRequest.KEEP_ALIVE_SIZE_LIMIT;
4446
const requestOptions: RequestInit = {
4547
method: 'POST',
4648
mode: 'cors',
@@ -50,6 +52,7 @@ export class NetRequest {
5052
'User-Agent': browserInfo.userAgent,
5153
},
5254
body: eventsJson,
55+
keepalive: isKeepAlive,
5356
};
5457
requestOptions.signal = controller.signal;
5558

src/provider/AnalyticsEventBuilder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class AnalyticsEventBuilder {
7878
sdk_version: sdkVersion,
7979
items: items,
8080
user: userAttributes ?? {},
81-
attributes: attributes ?? {},
81+
attributes: attributes,
8282
};
8383
analyticEvent.hashCode = await HashUtil.getHashCode(
8484
JSON.stringify(analyticEvent)

src/provider/ClickstreamProvider.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ export class ClickstreamProvider implements AnalyticsProvider {
9393
'Initialize the SDK successfully, configuration is:\n' +
9494
JSON.stringify(this.configuration)
9595
);
96-
this.eventRecorder.sendFailedEvents();
96+
if (this.eventRecorder.getFailedEventsLength() > 0) {
97+
this.eventRecorder.haveFailedEvents = true;
98+
this.eventRecorder.sendFailedEvents();
99+
}
97100
return this.configuration;
98101
}
99102

@@ -121,8 +124,8 @@ export class ClickstreamProvider implements AnalyticsProvider {
121124
this.userAttribute,
122125
this.sessionTracker.session
123126
)
124-
.then(event => {
125-
this.eventRecorder.record(event);
127+
.then(resultEvent => {
128+
this.eventRecorder.record(resultEvent, event.isImmediate);
126129
})
127130
.catch(error => {
128131
logger.error(`Create event fail with ${error}`);
@@ -191,4 +194,13 @@ export class ClickstreamProvider implements AnalyticsProvider {
191194
flushEvents(eventRecorder: EventRecorder) {
192195
eventRecorder.flushEvents();
193196
}
197+
198+
sendEventsInBackground(isWindowClosing: boolean) {
199+
if (
200+
!(BrowserInfo.isFirefox() && isWindowClosing) &&
201+
BrowserInfo.isNetworkOnLine()
202+
) {
203+
this.eventRecorder.sendEventsInBackground(isWindowClosing);
204+
}
205+
}
194206
}

src/provider/EventRecorder.ts

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,28 +24,29 @@ export class EventRecorder {
2424
context: ClickstreamContext;
2525
bundleSequenceId: number;
2626
isFlushingEvents: boolean;
27+
isSendingFailedEvents: boolean;
28+
haveFailedEvents: boolean;
2729

2830
constructor(context: ClickstreamContext) {
2931
this.context = context;
3032
this.bundleSequenceId = StorageUtil.getBundleSequenceId();
3133
}
3234

33-
record(event: AnalyticsEvent) {
35+
record(event: AnalyticsEvent, isImmediate = false) {
3436
if (this.context.configuration.isLogEvents) {
3537
logger.level = LOG_TYPE.DEBUG;
3638
logger.debug(
3739
`Logged event ${event.event_type}, event attributes:\n
3840
${JSON.stringify(event)}`
3941
);
4042
}
41-
switch (this.context.configuration.sendMode) {
42-
case SendMode.Immediate:
43+
const currentMode = this.context.configuration.sendMode;
44+
if (currentMode === SendMode.Immediate || isImmediate) {
45+
this.sendEventImmediate(event);
46+
} else {
47+
if (!StorageUtil.saveEvent(event)) {
4348
this.sendEventImmediate(event);
44-
break;
45-
case SendMode.Batch:
46-
if (!StorageUtil.saveEvent(event)) {
47-
this.sendEventImmediate(event);
48-
}
49+
}
4950
}
5051
}
5152

@@ -58,14 +59,20 @@ export class EventRecorder {
5859
).then(result => {
5960
if (result) {
6061
logger.debug('Event send success');
62+
if (this.haveFailedEvents) {
63+
this.sendFailedEvents();
64+
}
6165
} else {
6266
StorageUtil.saveFailedEvent(event);
67+
this.haveFailedEvents = true;
6368
}
6469
});
6570
this.plusSequenceId();
6671
}
6772

6873
sendFailedEvents() {
74+
if (this.isSendingFailedEvents) return;
75+
this.isSendingFailedEvents = true;
6976
const failedEvents = StorageUtil.getFailedEvents();
7077
if (failedEvents.length > 0) {
7178
const eventsJson = failedEvents + Event.Constants.SUFFIX;
@@ -77,7 +84,9 @@ export class EventRecorder {
7784
if (result) {
7885
logger.debug('Failed events send success');
7986
StorageUtil.clearFailedEvents();
87+
this.haveFailedEvents = false;
8088
}
89+
this.isSendingFailedEvents = false;
8190
});
8291
this.plusSequenceId();
8392
}
@@ -103,7 +112,7 @@ export class EventRecorder {
103112
StorageUtil.clearEvents(eventsJson);
104113
}
105114
this.isFlushingEvents = false;
106-
if (needsFlushTwice) {
115+
if (result && needsFlushTwice) {
107116
this.flushEvents();
108117
}
109118
});
@@ -147,4 +156,35 @@ export class EventRecorder {
147156
this.bundleSequenceId += 1;
148157
StorageUtil.saveBundleSequenceId(this.bundleSequenceId);
149158
}
159+
160+
sendEventsInBackground(isWindowClosing: boolean) {
161+
if (
162+
this.haveFailedEvents &&
163+
this.getFailedEventsLength() < NetRequest.KEEP_ALIVE_SIZE_LIMIT
164+
) {
165+
this.sendFailedEvents();
166+
if (isWindowClosing) {
167+
StorageUtil.clearFailedEvents();
168+
}
169+
}
170+
if (this.context.configuration.sendMode === SendMode.Batch) {
171+
const eventLength = this.getEventsLength();
172+
if (eventLength > 0 && eventLength < NetRequest.KEEP_ALIVE_SIZE_LIMIT) {
173+
this.flushEvents();
174+
if (isWindowClosing) {
175+
StorageUtil.clearAllEvents();
176+
}
177+
}
178+
}
179+
}
180+
181+
getFailedEventsLength(): number {
182+
const failedEvents = StorageUtil.getFailedEvents();
183+
return new Blob([failedEvents]).size;
184+
}
185+
186+
getEventsLength(): number {
187+
const events = StorageUtil.getAllEvents();
188+
return new Blob([events]).size;
189+
}
150190
}

src/tracker/SessionTracker.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import { Logger } from '@aws-amplify/core';
1414
import { BaseTracker } from './BaseTracker';
1515
import { Session } from './Session';
16+
import { BrowserInfo } from '../browser';
1617
import { Event } from '../provider';
1718
import { StorageUtil } from '../util/StorageUtil';
1819

@@ -23,6 +24,7 @@ export class SessionTracker extends BaseTracker {
2324
visibilityChange: string;
2425
session: Session;
2526
startEngageTimestamp: number;
27+
isWindowClosing = false;
2628

2729
init() {
2830
this.onVisibilityChange = this.onVisibilityChange.bind(this);
@@ -77,24 +79,28 @@ export class SessionTracker extends BaseTracker {
7779
onPageHide() {
7880
logger.debug('page hide');
7981
this.storeSession();
80-
this.recordUserEngagement();
82+
this.recordUserEngagement(
83+
!(this.isWindowClosing && BrowserInfo.isFirefox())
84+
);
85+
this.provider.sendEventsInBackground(this.isWindowClosing);
8186
}
8287

83-
recordUserEngagement() {
88+
recordUserEngagement(isImmediate = false) {
8489
const engagementTime = new Date().getTime() - this.startEngageTimestamp;
8590
if (engagementTime > Constants.minEngagementTime) {
8691
this.provider.record({
8792
name: Event.PresetEvent.USER_ENGAGEMENT,
8893
attributes: {
8994
[Event.ReservedAttribute.ENGAGEMENT_TIMESTAMP]: engagementTime,
9095
},
96+
isImmediate: isImmediate,
9197
});
9298
}
9399
}
94100

95101
onBeforeUnload() {
96102
logger.debug('onBeforeUnload');
97-
this.onPageHide();
103+
this.isWindowClosing = true;
98104
}
99105

100106
storeSession() {

src/types/Analytics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface ClickstreamEvent {
7676
name: string;
7777
attributes?: ClickstreamAttribute;
7878
items?: Item[];
79+
isImmediate?: boolean;
7980
}
8081

8182
export interface AnalyticsEvent {

src/util/StorageUtil.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ export class StorageUtil {
204204
}
205205

206206
static clearEvents(eventsJson: string) {
207+
const eventsString = this.getAllEvents();
208+
if (eventsString === '') return;
207209
const deletedEvents = JSON.parse(eventsJson);
208210
const allEvents = JSON.parse(this.getAllEvents() + Event.Constants.SUFFIX);
209211
if (allEvents.length > deletedEvents.length) {
@@ -216,6 +218,10 @@ export class StorageUtil {
216218
}
217219
}
218220

221+
static clearAllEvents() {
222+
localStorage.removeItem(StorageUtil.eventsKey);
223+
}
224+
219225
static saveSession(session: Session) {
220226
localStorage.setItem(StorageUtil.sessionKey, JSON.stringify(session));
221227
}

0 commit comments

Comments
 (0)