@mparticle/web-sdk 2.19.2 → 2.19.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,17 +9,35 @@ import { convertEvents } from './sdkToEventsApiConverter';
9
9
  import Types from './types';
10
10
  import { isEmpty } from './utils';
11
11
 
12
+ /**
13
+ * BatchUploader contains all the logic to upload batches to mParticle.
14
+ * It queues events as they come in and at set intervals turns them into batches.
15
+ * It then attempts to upload them to mParticle.
16
+ *
17
+ * These uploads happen on an interval basis using window.fetch or XHR
18
+ * requests, depending on what is available in the browser.
19
+ *
20
+ * Uploads can also be triggered on browser visibility/focus changes via an
21
+ * event listener, which then uploads to mPartice via the browser's Beacon API.
22
+ */
23
+
12
24
  export class BatchUploader {
13
- //we upload JSON, but this content type is required to avoid a CORS preflight request
25
+ // We upload JSON, but this content type is required to avoid a CORS preflight request
14
26
  static readonly CONTENT_TYPE: string = 'text/plain;charset=UTF-8';
15
27
  static readonly MINIMUM_INTERVAL_MILLIS: number = 500;
16
28
  uploadIntervalMillis: number;
17
- pendingEvents: SDKEvent[];
18
- pendingUploads: Batch[];
29
+ eventsQueuedForProcessing: SDKEvent[];
30
+ batchesQueuedForProcessing: Batch[];
19
31
  mpInstance: MParticleWebSDK;
20
32
  uploadUrl: string;
21
33
  batchingEnabled: boolean;
34
+ private uploader: AsyncUploader;
22
35
 
36
+ /**
37
+ * Creates an instance of a BatchUploader
38
+ * @param {MParticleWebSDK} mpInstance - the mParticle SDK instance
39
+ * @param {number} uploadInterval - the desired upload interval in milliseconds
40
+ */
23
41
  constructor(mpInstance: MParticleWebSDK, uploadInterval: number) {
24
42
  this.mpInstance = mpInstance;
25
43
  this.uploadIntervalMillis = uploadInterval;
@@ -28,8 +46,8 @@ export class BatchUploader {
28
46
  if (this.uploadIntervalMillis < BatchUploader.MINIMUM_INTERVAL_MILLIS) {
29
47
  this.uploadIntervalMillis = BatchUploader.MINIMUM_INTERVAL_MILLIS;
30
48
  }
31
- this.pendingEvents = [];
32
- this.pendingUploads = [];
49
+ this.eventsQueuedForProcessing = [];
50
+ this.batchesQueuedForProcessing = [];
33
51
 
34
52
  const { SDKConfig, devToken } = this.mpInstance._Store;
35
53
  const baseUrl = this.mpInstance._Helpers.createServiceUrl(
@@ -38,15 +56,20 @@ export class BatchUploader {
38
56
  );
39
57
  this.uploadUrl = `${baseUrl}/events`;
40
58
 
41
- setTimeout(() => {
42
- this.prepareAndUpload(true, false);
43
- }, this.uploadIntervalMillis);
59
+ this.uploader = window.fetch
60
+ ? new FetchUploader(this.uploadUrl)
61
+ : new XHRUploader(this.uploadUrl);
62
+
63
+ this.triggerUploadInterval(true, false);
44
64
  this.addEventListeners();
45
65
  }
46
66
 
67
+ // Adds listeners to be used trigger Navigator.sendBeacon if the browser
68
+ // loses focus for any reason, such as closing browser tab or minimizing window
47
69
  private addEventListeners() {
48
70
  const _this = this;
49
71
 
72
+ // visibility change is a document property, not window
50
73
  document.addEventListener('visibilitychange', () => {
51
74
  _this.prepareAndUpload(false, _this.isBeaconAvailable());
52
75
  });
@@ -65,16 +88,32 @@ export class BatchUploader {
65
88
  return false;
66
89
  }
67
90
 
91
+ // Triggers a setTimeout for prepareAndUpload
92
+ private triggerUploadInterval(
93
+ triggerFuture: boolean = false,
94
+ useBeacon: boolean = false
95
+ ): void {
96
+ setTimeout(() => {
97
+ this.prepareAndUpload(triggerFuture, useBeacon);
98
+ }, this.uploadIntervalMillis);
99
+ }
100
+
101
+ /**
102
+ * This method will queue a single Event which will eventually be processed into a Batch
103
+ * @param event event that should be queued
104
+ */
68
105
  queueEvent(event: SDKEvent): void {
69
106
  if (!isEmpty(event)) {
70
- this.pendingEvents.push(event);
107
+ this.eventsQueuedForProcessing.push(event);
71
108
  this.mpInstance.Logger.verbose(
72
109
  `Queuing event: ${JSON.stringify(event)}`
73
110
  );
74
111
  this.mpInstance.Logger.verbose(
75
- `Queued event count: ${this.pendingEvents.length}`
112
+ `Queued event count: ${this.eventsQueuedForProcessing.length}`
76
113
  );
77
114
 
115
+ // TODO: Remove this check once the v2 code path is removed
116
+ // https://go.mparticle.com/work/SQDSDKS-3720
78
117
  if (
79
118
  !this.batchingEnabled ||
80
119
  Types.TriggerUploadType[event.EventDataType]
@@ -93,7 +132,7 @@ export class BatchUploader {
93
132
  * @param sdkEvents current pending events
94
133
  * @param defaultUser the user to reference for events that are missing data
95
134
  */
96
- private static createNewUploads(
135
+ private static createNewBatches(
97
136
  sdkEvents: SDKEvent[],
98
137
  defaultUser: MParticleUser,
99
138
  mpInstance: MParticleWebSDK
@@ -141,9 +180,8 @@ export class BatchUploader {
141
180
  mpInstance._Store.SDKConfig.onCreateBatch;
142
181
 
143
182
  if (onCreateBatchCallback) {
144
- uploadBatchObject = onCreateBatchCallback(
145
- uploadBatchObject
146
- );
183
+ uploadBatchObject =
184
+ onCreateBatchCallback(uploadBatchObject);
147
185
  if (uploadBatchObject) {
148
186
  uploadBatchObject.modified = true;
149
187
  } else {
@@ -169,47 +207,58 @@ export class BatchUploader {
169
207
  * @param triggerFuture whether to trigger the loop again - for manual/forced uploads this should be false
170
208
  * @param useBeacon whether to use the beacon API - used when the page is being unloaded
171
209
  */
172
- private async prepareAndUpload(triggerFuture: boolean, useBeacon: boolean) {
210
+ private async prepareAndUpload(
211
+ triggerFuture: boolean,
212
+ useBeacon: boolean
213
+ ): Promise<void> {
214
+ // Fetch current user so that events can be grouped by MPID
173
215
  const currentUser = this.mpInstance.Identity.getCurrentUser();
174
216
 
175
- const currentEvents = this.pendingEvents;
176
- this.pendingEvents = [];
177
- const newUploads = BatchUploader.createNewUploads(
217
+ const currentEvents = this.eventsQueuedForProcessing;
218
+ this.eventsQueuedForProcessing = [];
219
+
220
+ const newBatches = BatchUploader.createNewBatches(
178
221
  currentEvents,
179
222
  currentUser,
180
223
  this.mpInstance
181
224
  );
182
- if (newUploads && newUploads.length) {
183
- this.pendingUploads.push(...newUploads);
225
+
226
+ if (!isEmpty(newBatches)) {
227
+ this.batchesQueuedForProcessing.push(...newBatches);
184
228
  }
185
229
 
186
- const currentUploads = this.pendingUploads;
187
- this.pendingUploads = [];
188
- const remainingUploads: Batch[] = await this.upload(
230
+ // Clear out pending batches to avoid any potential duplication
231
+ const batchesToUpload = this.batchesQueuedForProcessing;
232
+ this.batchesQueuedForProcessing = [];
233
+
234
+ const batchesThatDidNotUpload = await this.uploadBatches(
189
235
  this.mpInstance.Logger,
190
- currentUploads,
236
+ batchesToUpload,
191
237
  useBeacon
192
238
  );
193
- if (remainingUploads && remainingUploads.length) {
194
- this.pendingUploads.unshift(...remainingUploads);
239
+
240
+ // Batches that do not successfully upload are added back to the process queue
241
+ // in the order they were created so that we can attempt re-transmission in
242
+ // the same sequence. This is to prevent any potential data corruption.
243
+ if (!isEmpty(batchesThatDidNotUpload)) {
244
+ // TODO: https://go.mparticle.com/work/SQDSDKS-5165
245
+ this.batchesQueuedForProcessing.unshift(...batchesThatDidNotUpload);
195
246
  }
196
247
 
197
248
  if (triggerFuture) {
198
- setTimeout(() => {
199
- this.prepareAndUpload(true, false);
200
- }, this.uploadIntervalMillis);
249
+ this.triggerUploadInterval(triggerFuture, false);
201
250
  }
202
251
  }
203
252
 
204
- private async upload(
253
+ // TODO: Refactor to use logger as a class method
254
+ // https://go.mparticle.com/work/SQDSDKS-5167
255
+ private async uploadBatches(
205
256
  logger: SDKLoggerApi,
206
- _uploads: Batch[],
257
+ batches: Batch[],
207
258
  useBeacon: boolean
208
- ): Promise<Batch[]> {
209
- let uploader;
210
-
259
+ ): Promise<Batch[] | null> {
211
260
  // Filter out any batches that don't have events
212
- const uploads = _uploads.filter(upload => !isEmpty(upload.events));
261
+ const uploads = batches.filter((batch) => !isEmpty(batch.events));
213
262
 
214
263
  if (isEmpty(uploads)) {
215
264
  return null;
@@ -235,19 +284,8 @@ export class BatchUploader {
235
284
  });
236
285
  navigator.sendBeacon(this.uploadUrl, blob);
237
286
  } else {
238
- if (!uploader) {
239
- if (window.fetch) {
240
- uploader = new FetchUploader(this.uploadUrl, logger);
241
- } else {
242
- uploader = new XHRUploader(this.uploadUrl, logger);
243
- }
244
- }
245
287
  try {
246
- const response = await uploader.upload(
247
- fetchPayload,
248
- uploads,
249
- i
250
- );
288
+ const response = await this.uploader.upload(fetchPayload);
251
289
 
252
290
  if (response.status >= 200 && response.status < 300) {
253
291
  logger.verbose(
@@ -260,7 +298,7 @@ export class BatchUploader {
260
298
  logger.error(
261
299
  `HTTP error status ${response.status} received`
262
300
  );
263
- //server error, add back current events and try again later
301
+ // Server error, add back current batches and try again later
264
302
  return uploads.slice(i, uploads.length);
265
303
  } else if (response.status >= 401) {
266
304
  logger.error(
@@ -268,6 +306,16 @@ export class BatchUploader {
268
306
  );
269
307
  //if we're getting a 401, assume we'll keep getting a 401 and clear the uploads.
270
308
  return null;
309
+ } else {
310
+ // In case there is an HTTP error we did not anticipate.
311
+ console.error(
312
+ `HTTP error status ${response.status} while uploading events.`,
313
+ response
314
+ );
315
+
316
+ throw new Error(
317
+ `Uncaught HTTP Error ${response.status}. Batch upload will be re-attempted.`
318
+ );
271
319
  }
272
320
  } catch (e) {
273
321
  logger.error(
@@ -283,34 +331,24 @@ export class BatchUploader {
283
331
 
284
332
  abstract class AsyncUploader {
285
333
  url: string;
286
- logger: SDKLoggerApi;
334
+ public abstract upload(fetchPayload: fetchPayload): Promise<XHRResponse>;
287
335
 
288
- constructor(url: string, logger: SDKLoggerApi) {
336
+ constructor(url: string) {
289
337
  this.url = url;
290
- this.logger = logger;
291
338
  }
292
339
  }
293
340
 
294
341
  class FetchUploader extends AsyncUploader {
295
- private async upload(
296
- fetchPayload: fetchPayload,
297
- uploads: Batch[],
298
- i: number
299
- ) {
342
+ public async upload(fetchPayload: fetchPayload): Promise<XHRResponse> {
300
343
  const response: XHRResponse = await fetch(this.url, fetchPayload);
301
344
  return response;
302
345
  }
303
346
  }
304
347
 
305
348
  class XHRUploader extends AsyncUploader {
306
- private async upload(
307
- fetchPayload: fetchPayload,
308
- uploads: Batch[],
309
- i: number
310
- ) {
349
+ public async upload(fetchPayload: fetchPayload): Promise<XHRResponse> {
311
350
  const response: XHRResponse = await this.makeRequest(
312
351
  this.url,
313
- this.logger,
314
352
  fetchPayload.body
315
353
  );
316
354
  return response;
@@ -318,7 +356,6 @@ class XHRUploader extends AsyncUploader {
318
356
 
319
357
  private async makeRequest(
320
358
  url: string,
321
- logger: SDKLoggerApi,
322
359
  data: string
323
360
  ): Promise<XMLHttpRequest> {
324
361
  const xhr: XMLHttpRequest = new XMLHttpRequest();
package/src/forwarders.js CHANGED
@@ -3,6 +3,12 @@ import filteredMparticleUser from './filteredMparticleUser';
3
3
 
4
4
  export default function Forwarders(mpInstance, kitBlocker) {
5
5
  var self = this;
6
+
7
+ const UserAttributeActionTypes = {
8
+ setUserAttribute: 'setUserAttribute',
9
+ removeUserAttribute: 'removeUserAttribute',
10
+ };
11
+
6
12
  this.initForwarders = function(userIdentities, forwardingStatsCallback) {
7
13
  var user = mpInstance.Identity.getCurrentUser();
8
14
  if (
@@ -387,33 +393,47 @@ export default function Forwarders(mpInstance, kitBlocker) {
387
393
  }
388
394
  };
389
395
 
390
- this.callSetUserAttributeOnForwarders = function(key, value) {
391
- if (kitBlocker && kitBlocker.isAttributeKeyBlocked(key)) {
396
+ this.handleForwarderUserAttributes = function(functionNameKey, key, value) {
397
+ if (
398
+ (kitBlocker && kitBlocker.isAttributeKeyBlocked(key)) ||
399
+ !mpInstance._Store.activeForwarders.length
400
+ ) {
392
401
  return;
393
402
  }
394
403
 
395
- if (mpInstance._Store.activeForwarders.length) {
396
- mpInstance._Store.activeForwarders.forEach(function(forwarder) {
404
+ mpInstance._Store.activeForwarders.forEach(function(forwarder) {
405
+ const forwarderFunction = forwarder[functionNameKey];
406
+ if (
407
+ !forwarderFunction ||
408
+ mpInstance._Helpers.isFilteredUserAttribute(
409
+ key,
410
+ forwarder.userAttributeFilters
411
+ )
412
+ ) {
413
+ return;
414
+ }
415
+ try {
416
+ let result;
417
+
397
418
  if (
398
- forwarder.setUserAttribute &&
399
- forwarder.userAttributeFilters &&
400
- !mpInstance._Helpers.inArray(
401
- forwarder.userAttributeFilters,
402
- mpInstance._Helpers.generateHash(key)
403
- )
419
+ functionNameKey ===
420
+ UserAttributeActionTypes.setUserAttribute
404
421
  ) {
405
- try {
406
- var result = forwarder.setUserAttribute(key, value);
422
+ result = forwarder.setUserAttribute(key, value);
423
+ } else if (
424
+ functionNameKey ===
425
+ UserAttributeActionTypes.removeUserAttribute
426
+ ) {
427
+ result = forwarder.removeUserAttribute(key);
428
+ }
407
429
 
408
- if (result) {
409
- mpInstance.Logger.verbose(result);
410
- }
411
- } catch (e) {
412
- mpInstance.Logger.error(e);
413
- }
430
+ if (result) {
431
+ mpInstance.Logger.verbose(result);
414
432
  }
415
- });
416
- }
433
+ } catch (e) {
434
+ mpInstance.Logger.error(e);
435
+ }
436
+ });
417
437
  };
418
438
 
419
439
  this.setForwarderUserIdentities = function(userIdentities) {
package/src/helpers.js CHANGED
@@ -387,6 +387,11 @@ export default function Helpers(mpInstance) {
387
387
  return filteredUserAttributes;
388
388
  };
389
389
 
390
+ this.isFilteredUserAttribute = function(userAttributeKey, filterList) {
391
+ const hashedUserAttribute = self.generateHash(userAttributeKey);
392
+ return filterList && self.inArray(filterList, hashedUserAttribute);
393
+ };
394
+
390
395
  this.isEventType = function(type) {
391
396
  for (var prop in Types.EventType) {
392
397
  if (Types.EventType.hasOwnProperty(prop)) {
package/src/identity.js CHANGED
@@ -904,7 +904,8 @@ export default function Identity(mpInstance) {
904
904
  self.IdentityAPI.getCurrentUser().getUserIdentities(),
905
905
  mpInstance._APIClient.prepareForwardingStats
906
906
  );
907
- mpInstance._Forwarders.callSetUserAttributeOnForwarders(
907
+ mpInstance._Forwarders.handleForwarderUserAttributes(
908
+ 'setUserAttribute',
908
909
  key,
909
910
  newValue
910
911
  );
@@ -990,9 +991,10 @@ export default function Identity(mpInstance) {
990
991
  self.IdentityAPI.getCurrentUser().getUserIdentities(),
991
992
  mpInstance._APIClient.prepareForwardingStats
992
993
  );
993
- mpInstance._Forwarders.applyToForwarders(
994
+ mpInstance._Forwarders.handleForwarderUserAttributes(
994
995
  'removeUserAttribute',
995
- key
996
+ key,
997
+ null
996
998
  );
997
999
  }
998
1000
  },
@@ -1098,7 +1100,8 @@ export default function Identity(mpInstance) {
1098
1100
  self.IdentityAPI.getCurrentUser().getUserIdentities(),
1099
1101
  mpInstance._APIClient.prepareForwardingStats
1100
1102
  );
1101
- mpInstance._Forwarders.callSetUserAttributeOnForwarders(
1103
+ mpInstance._Forwarders.handleForwarderUserAttributes(
1104
+ 'setUserAttribute',
1102
1105
  key,
1103
1106
  arrayCopy
1104
1107
  );
@@ -1127,9 +1130,10 @@ export default function Identity(mpInstance) {
1127
1130
  if (userAttributes) {
1128
1131
  for (var prop in userAttributes) {
1129
1132
  if (userAttributes.hasOwnProperty(prop)) {
1130
- mpInstance._Forwarders.applyToForwarders(
1133
+ mpInstance._Forwarders.handleForwarderUserAttributes(
1131
1134
  'removeUserAttribute',
1132
- prop
1135
+ prop,
1136
+ null
1133
1137
  );
1134
1138
  }
1135
1139
  this.removeUserAttribute(prop);
package/src/vault.ts CHANGED
@@ -6,12 +6,10 @@ interface IVaultOptions {
6
6
  offlineStorageEnabled?: boolean;
7
7
  }
8
8
 
9
- export default class Vault<StorableItem extends Dictionary> {
10
- public contents: Dictionary<StorableItem>;
9
+ export default class Vault<StorableItem> {
10
+ public contents: StorableItem;
11
11
  private readonly _storageKey: string;
12
- private readonly _itemKey: keyof StorableItem;
13
12
  private logger?: Logger;
14
- private offlineStorageEnabled: boolean = false;
15
13
 
16
14
  /**
17
15
  *
@@ -19,16 +17,9 @@ export default class Vault<StorableItem extends Dictionary> {
19
17
  * @param {string} itemKey an element within your StorableItem to use as a key
20
18
  * @param {IVaultOptions} options A Dictionary of IVaultOptions
21
19
  */
22
- constructor(
23
- storageKey: string,
24
- itemKey: keyof StorableItem,
25
- options?: IVaultOptions
26
- ) {
20
+ constructor(storageKey: string, options?: IVaultOptions) {
27
21
  this._storageKey = storageKey;
28
- this._itemKey = itemKey;
29
- this.contents = this.getItems() || {};
30
-
31
- this.offlineStorageEnabled = options?.offlineStorageEnabled;
22
+ this.contents = this.getFromLocalStorage();
32
23
 
33
24
  // Add a fake logger in case one is not provided or needed
34
25
  this.logger = options?.logger || {
@@ -39,51 +30,16 @@ export default class Vault<StorableItem extends Dictionary> {
39
30
  }
40
31
 
41
32
  /**
42
- * Stores a single Item using `itemId` as an index
33
+ * Stores a StorableItem to Local Storage
43
34
  * @method storeItem
44
- * @param item {StorableItem} a Dictonary with key to store
45
- */
46
- public storeItem(item: StorableItem): void {
47
- this.contents = this.getItems();
48
-
49
- if (item[this._itemKey]) {
50
- this.contents[item[this._itemKey]] = item;
51
- this.logger.verbose(
52
- `Saved items to vault with key: ${item[this._itemKey]}`
53
- );
54
- }
55
-
56
- this.saveItems(this.contents);
57
- }
58
-
59
- /**
60
- * Stores Items using `itemId` as an index
61
- * @method storeItems
62
- * @param {StorableItem[]} an Array of StorableItems
63
- */
64
- public storeItems(items: StorableItem[]): void {
65
- items.forEach((item) => this.storeItem(item));
66
- }
67
-
68
- /**
69
- * Removes a single StorableItem based on `indexId`
70
- * @method removeItem
71
- * @param {String} indexId
35
+ * @param item {StorableItem}
72
36
  */
73
- public removeItem(indexId: string): void {
74
- this.logger.verbose(`Removing from vault: ${indexId}`);
75
- this.contents = this.getItems() || {};
37
+ public store(item: StorableItem): void {
38
+ this.contents = item;
76
39
 
77
- try {
78
- delete this.contents[indexId];
40
+ this.logger.verbose(`Saved to local storage: ${item}`);
79
41
 
80
- this.saveItems(this.contents);
81
- } catch (error) {
82
- this.logger.error(
83
- `Unable to remove item without a matching ID ${indexId}`
84
- );
85
- this.logger.error(error as string);
86
- }
42
+ this.saveToLocalStorage(this.contents);
87
43
  }
88
44
 
89
45
  /**
@@ -91,10 +47,10 @@ export default class Vault<StorableItem extends Dictionary> {
91
47
  * @method retrieveItems
92
48
  * @returns {StorableItem[]} an array of Items
93
49
  */
94
- public retrieveItems(): StorableItem[] {
95
- this.contents = this.getItems();
50
+ public retrieve(): StorableItem {
51
+ this.contents = this.getFromLocalStorage();
96
52
 
97
- return Object.keys(this.contents).map((item) => this.contents[item]);
53
+ return this.contents;
98
54
  }
99
55
 
100
56
  /**
@@ -102,14 +58,11 @@ export default class Vault<StorableItem extends Dictionary> {
102
58
  * @method purge
103
59
  */
104
60
  public purge(): void {
105
- this.contents = {};
106
- this.removeItems();
61
+ this.contents = null;
62
+ this.removeFromLocalStorage();
107
63
  }
108
64
 
109
- private saveItems(items: Dictionary<StorableItem>): void {
110
- if (!this.offlineStorageEnabled) {
111
- return;
112
- }
65
+ private saveToLocalStorage(items: StorableItem): void {
113
66
  try {
114
67
  window.localStorage.setItem(
115
68
  this._storageKey,
@@ -121,21 +74,15 @@ export default class Vault<StorableItem extends Dictionary> {
121
74
  }
122
75
  }
123
76
 
124
- private getItems(): Dictionary<StorableItem> {
125
- if (!this.offlineStorageEnabled) {
126
- return this.contents;
127
- }
77
+ private getFromLocalStorage(): StorableItem | null {
128
78
  // TODO: Handle cases where Local Storage is unavailable
129
79
  // https://go.mparticle.com/work/SQDSDKS-5022
130
- const itemString = window.localStorage.getItem(this._storageKey);
80
+ const item: string = window.localStorage.getItem(this._storageKey);
131
81
 
132
- return itemString ? JSON.parse(itemString) : {};
82
+ return item ? JSON.parse(item) : null;
133
83
  }
134
84
 
135
- private removeItems(): void {
136
- if (!this.offlineStorageEnabled) {
137
- return;
138
- }
85
+ private removeFromLocalStorage(): void {
139
86
  window.localStorage.removeItem(this._storageKey);
140
87
  }
141
- }
88
+ }