Skip to content

Commit 147bad2

Browse files
committed
feat: Fire background AST events on page transitions if feature flag enabled (#991)
1 parent 1df26c8 commit 147bad2

15 files changed

+962
-303
lines changed

src/apiClient.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { MPForwarder } from './forwarders.interfaces';
99
import { IMParticleUser, ISDKUserAttributes } from './identity-user-interfaces';
1010
import { AsyncUploader, FetchUploader, XHRUploader } from './uploaders';
1111
import { IMParticleWebSDKInstance } from './mp-instance';
12+
import { appendUserInfo } from './user-utils';
1213

1314
export interface IAPIClient {
1415
uploader: BatchUploader | null;
@@ -82,7 +83,7 @@ export default function APIClient(
8283
this.appendUserInfoToEvents = function(user, events) {
8384
events.forEach(function(event) {
8485
if (!event.MPID) {
85-
mpInstance._ServerModel.appendUserInfo(user, event);
86+
appendUserInfo(user, event);
8687
}
8788
});
8889
};

src/batchUploader.ts

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Batch } from '@mparticle/event-models';
22
import Constants from './constants';
33
import { SDKEvent, SDKLoggerApi } from './sdkRuntimeModels';
44
import { convertEvents } from './sdkToEventsApiConverter';
5-
import { MessageType } from './types';
5+
import { MessageType, EventType } from './types';
66
import { getRampNumber, isEmpty } from './utils';
77
import { SessionStorageVault, LocalStorageVault } from './vault';
88
import {
@@ -13,7 +13,7 @@ import {
1313
} from './uploaders';
1414
import { IMParticleUser } from './identity-user-interfaces';
1515
import { IMParticleWebSDKInstance } from './mp-instance';
16-
16+
import { appendUserInfo } from './user-utils';
1717
/**
1818
* BatchUploader contains all the logic to store/retrieve events and batches
1919
* to/from persistence, and upload batches to mParticle.
@@ -44,6 +44,8 @@ export class BatchUploader {
4444
private batchVault: LocalStorageVault<Batch[]>;
4545
private offlineStorageEnabled: boolean = false;
4646
private uploader: AsyncUploader;
47+
private lastASTEventTime: number = 0;
48+
private readonly AST_DEBOUNCE_MS: number = 1000; // 1 second debounce
4749

4850
/**
4951
* Creates an instance of a BatchUploader
@@ -129,21 +131,79 @@ export class BatchUploader {
129131
return offlineStoragePercentage >= rampNumber;
130132
}
131133

134+
// debounce AST just in case multiple events are fired in a short period of time due to browser differences
135+
private shouldDebounceAndUpdateLastASTTime(): boolean {
136+
const now = Date.now();
137+
if (now - this.lastASTEventTime < this.AST_DEBOUNCE_MS) {
138+
return true;
139+
}
140+
141+
this.lastASTEventTime = now;
142+
return false;
143+
}
144+
145+
// https://go.mparticle.com/work/SQDSDKS-7133
146+
private createBackgroundASTEvent(): SDKEvent {
147+
const now = Date.now();
148+
const { _Store, Identity, _timeOnSiteTimer, _Helpers } = this.mpInstance;
149+
const { sessionId, deviceId, sessionStartDate, SDKConfig } = _Store;
150+
const { generateUniqueId } = _Helpers;
151+
const { getCurrentUser } = Identity;
152+
153+
const event = {
154+
AppName: SDKConfig.appName,
155+
AppVersion: SDKConfig.appVersion,
156+
Package: SDKConfig.package,
157+
EventDataType: MessageType.AppStateTransition,
158+
Timestamp: now,
159+
SessionId: sessionId,
160+
DeviceId: deviceId,
161+
IsFirstRun: false,
162+
SourceMessageId: generateUniqueId(),
163+
SDKVersion: Constants.sdkVersion,
164+
CustomFlags: {},
165+
EventAttributes: {},
166+
SessionStartDate: sessionStartDate?.getTime() || now,
167+
Debug: SDKConfig.isDevelopmentMode,
168+
ActiveTimeOnSite: _timeOnSiteTimer?.getTimeInForeground() || 0,
169+
IsBackgroundAST: true
170+
} as SDKEvent;
171+
172+
appendUserInfo(getCurrentUser(), event);
173+
return event;
174+
}
175+
132176
// Adds listeners to be used trigger Navigator.sendBeacon if the browser
133177
// loses focus for any reason, such as closing browser tab or minimizing window
134178
private addEventListeners() {
135179
const _this = this;
136180

181+
const handleExit = () => {
182+
// Check for debounce before creating and queueing event
183+
const {
184+
_Helpers: { getFeatureFlag },
185+
} = this.mpInstance;
186+
const { AstBackgroundEvents } = Constants.FeatureFlags;
187+
188+
if (getFeatureFlag(AstBackgroundEvents)) {
189+
if (_this.shouldDebounceAndUpdateLastASTTime()) {
190+
return;
191+
}
192+
// Add application state transition event to queue
193+
const event = _this.createBackgroundASTEvent();
194+
_this.queueEvent(event);
195+
}
196+
// Then trigger the upload with beacon
197+
_this.prepareAndUpload(false, _this.isBeaconAvailable());
198+
};
137199
// visibility change is a document property, not window
138200
document.addEventListener('visibilitychange', () => {
139-
_this.prepareAndUpload(false, _this.isBeaconAvailable());
140-
});
141-
window.addEventListener('beforeunload', () => {
142-
_this.prepareAndUpload(false, _this.isBeaconAvailable());
143-
});
144-
window.addEventListener('pagehide', () => {
145-
_this.prepareAndUpload(false, _this.isBeaconAvailable());
201+
if (document.visibilityState === 'hidden') {
202+
handleExit();
203+
}
146204
});
205+
window.addEventListener('beforeunload', handleExit);
206+
window.addEventListener('pagehide', handleExit);
147207
}
148208

149209
private isBeaconAvailable(): boolean {
@@ -387,6 +447,7 @@ export class BatchUploader {
387447
let blob = new Blob([fetchPayload.body], {
388448
type: 'text/plain;charset=UTF-8',
389449
});
450+
390451
navigator.sendBeacon(this.uploadUrl, blob);
391452
} else {
392453
try {

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ const Constants = {
175175
CacheIdentity: 'cacheIdentity',
176176
AudienceAPI: 'audienceAPI',
177177
CaptureIntegrationSpecificIds: 'captureIntegrationSpecificIds',
178+
AstBackgroundEvents: 'astBackgroundEvents',
178179
},
179180
DefaultInstance: 'default_instance',
180181
CCPAPurpose: 'data_sale_opt_out',

src/sdkRuntimeModels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export interface SDKEvent {
8181
LaunchReferral?: string;
8282
ExpandedEventCount: number;
8383
ActiveTimeOnSite: number;
84+
IsBackgroundAST?: boolean;
8485
}
8586

8687
export interface SDKGeoLocation {

src/sdkToEventsApiConverter.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,11 +489,13 @@ export function convertAST(
489489
const commonEventData: EventsApi.CommonEventData = convertBaseEventData(
490490
sdkEvent
491491
);
492+
493+
// Determine the transition type based on IsBackgroundAST flag
494+
const { applicationBackground, applicationInitialized } = EventsApi.ApplicationStateTransitionEventDataApplicationTransitionTypeEnum;
495+
const transitionType = sdkEvent.IsBackgroundAST ? applicationBackground : applicationInitialized;
496+
492497
let astEventData: EventsApi.ApplicationStateTransitionEventData = {
493-
application_transition_type:
494-
EventsApi
495-
.ApplicationStateTransitionEventDataApplicationTransitionTypeEnum
496-
.applicationInitialized,
498+
application_transition_type: transitionType,
497499
is_first_run: sdkEvent.IsFirstRun,
498500
is_upgrade: false,
499501
launch_referral: sdkEvent.LaunchReferral,

src/serverModel.ts

Lines changed: 2 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from './consent';
2828
import { IMParticleUser, ISDKUserIdentity } from './identity-user-interfaces';
2929
import { IMParticleWebSDKInstance } from './mp-instance';
30+
import { appendUserInfo } from './user-utils';
3031

3132
const MessageType = Types.MessageType;
3233
const ApplicationTransitionType = Types.ApplicationTransitionType;
@@ -140,7 +141,6 @@ export interface IServerModel {
140141
convertEventToV2DTO: (event: IUploadObject) => IServerV2DTO;
141142
createEventObject: (event: BaseEvent, user?: IMParticleUser) => SDKEvent;
142143
convertToConsentStateV2DTO: (state: SDKConsentState) => IConsentStateV2DTO;
143-
appendUserInfo: (user: IMParticleUser, event: SDKEvent) => void;
144144
}
145145

146146
// TODO: Make this a pure function that returns a new object
@@ -201,51 +201,6 @@ export default function ServerModel(
201201
) {
202202
var self = this;
203203

204-
// TODO: Can we refactor this to not mutate the event?
205-
this.appendUserInfo = function(
206-
user: IMParticleUser,
207-
event: SDKEvent
208-
): void {
209-
if (!event) {
210-
return;
211-
}
212-
if (!user) {
213-
event.MPID = null;
214-
event.ConsentState = null;
215-
event.UserAttributes = null;
216-
event.UserIdentities = null;
217-
return;
218-
}
219-
if (event.MPID && event.MPID === user.getMPID()) {
220-
return;
221-
}
222-
event.MPID = user.getMPID();
223-
event.ConsentState = user.getConsentState();
224-
event.UserAttributes = user.getAllUserAttributes();
225-
226-
var userIdentities = user.getUserIdentities().userIdentities;
227-
var dtoUserIdentities = {};
228-
for (var identityKey in userIdentities) {
229-
var identityType = Types.IdentityType.getIdentityType(identityKey);
230-
if (identityType !== false) {
231-
dtoUserIdentities[identityType] = userIdentities[identityKey];
232-
}
233-
}
234-
235-
var validUserIdentities = [];
236-
if (mpInstance._Helpers.isObject(dtoUserIdentities)) {
237-
if (Object.keys(dtoUserIdentities).length) {
238-
for (var key in dtoUserIdentities) {
239-
var userIdentity: Partial<ISDKUserIdentity> = {};
240-
userIdentity.Identity = dtoUserIdentities[key];
241-
userIdentity.Type = mpInstance._Helpers.parseNumber(key);
242-
validUserIdentities.push(userIdentity);
243-
}
244-
}
245-
}
246-
event.UserIdentities = validUserIdentities;
247-
};
248-
249204
this.convertToConsentStateV2DTO = function(
250205
state: SDKConsentState
251206
): IConsentStateV2DTO {
@@ -397,7 +352,7 @@ export default function ServerModel(
397352
// FIXME: Remove duplicate occurence
398353
eventObject.CurrencyCode = mpInstance._Store.currencyCode;
399354
var currentUser = user || mpInstance.Identity.getCurrentUser();
400-
self.appendUserInfo(currentUser, eventObject as SDKEvent);
355+
appendUserInfo(currentUser, eventObject as SDKEvent);
401356

402357
if (event.messageType === Types.MessageType.SessionEnd) {
403358
eventObject.SessionLength =

src/store.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ export interface IFeatureFlags {
138138
offlineStorage?: string;
139139
directURLRouting?: boolean;
140140
cacheIdentity?: boolean;
141+
captureIntegrationSpecificIds?: boolean;
142+
astBackgroundEvents?: boolean;
141143
}
142144

143145
// Temporary Interface until Store can be refactored as a class
@@ -714,6 +716,7 @@ export function processFlags(config: SDKInitConfig): IFeatureFlags {
714716
CacheIdentity,
715717
AudienceAPI,
716718
CaptureIntegrationSpecificIds,
719+
AstBackgroundEvents
717720
} = Constants.FeatureFlags;
718721

719722
if (!config.flags) {
@@ -732,6 +735,7 @@ export function processFlags(config: SDKInitConfig): IFeatureFlags {
732735
flags[CacheIdentity] = config.flags[CacheIdentity] === 'True';
733736
flags[AudienceAPI] = config.flags[AudienceAPI] === 'True';
734737
flags[CaptureIntegrationSpecificIds] = config.flags[CaptureIntegrationSpecificIds] === 'True';
738+
flags[AstBackgroundEvents] = config.flags[AstBackgroundEvents] === 'True';
735739

736740
return flags;
737741
}

src/user-utils.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { IMParticleUser, IdentityResultBody } from './identity-user-interfaces';
1+
import { UserIdentities } from '@mparticle/web-sdk';
2+
import { IMParticleUser, ISDKUserIdentity, IdentityResultBody } from './identity-user-interfaces';
3+
import { SDKEvent } from './sdkRuntimeModels';
4+
import Types from './types';
5+
import { isObject, parseNumber } from './utils';
26

37
export function hasMPIDAndUserLoginChanged(
48
previousUser: IMParticleUser,
@@ -23,3 +27,49 @@ export function hasMPIDChanged(
2327
identityApiResult.mpid !== prevUser.getMPID())
2428
);
2529
}
30+
31+
// https://go.mparticle.com/work/SQDSDKS-7136
32+
export function appendUserInfo(
33+
user: IMParticleUser,
34+
event: SDKEvent
35+
): void {
36+
if (!event) {
37+
return;
38+
}
39+
if (!user) {
40+
event.MPID = null;
41+
event.ConsentState = null;
42+
event.UserAttributes = null;
43+
event.UserIdentities = null;
44+
return;
45+
}
46+
if (event.MPID && event.MPID === user.getMPID()) {
47+
return;
48+
}
49+
50+
event.MPID = user.getMPID();
51+
event.ConsentState = user.getConsentState();
52+
event.UserAttributes = user.getAllUserAttributes();
53+
54+
const userIdentities: UserIdentities = user.getUserIdentities().userIdentities;
55+
const dtoUserIdentities = {};
56+
for (const identityKey in userIdentities) {
57+
const identityType = Types.IdentityType.getIdentityType(identityKey);
58+
if (identityType !== false) {
59+
dtoUserIdentities[identityType] = userIdentities[identityKey];
60+
}
61+
}
62+
63+
const validUserIdentities: ISDKUserIdentity[] = [];
64+
if (isObject(dtoUserIdentities)) {
65+
if (Object.keys(dtoUserIdentities).length) {
66+
for (const key in dtoUserIdentities) {
67+
const userIdentity = {} as ISDKUserIdentity;
68+
userIdentity.Identity = dtoUserIdentities[key];
69+
userIdentity.Type = parseNumber(key);
70+
validUserIdentities.push(userIdentity);
71+
}
72+
}
73+
}
74+
event.UserIdentities = validUserIdentities;
75+
}

0 commit comments

Comments
 (0)