@mparticle/web-sdk 3.11.1 → 3.12.0

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.
package/src/identity.ts CHANGED
@@ -1498,10 +1498,6 @@ export default function Identity(
1498
1498
  mpInstance._Persistence.update();
1499
1499
  mpInstance._Store.syncPersistenceData();
1500
1500
 
1501
- mpInstance._Persistence.findPrevCookiesBasedOnUI(
1502
- identityApiData
1503
- );
1504
-
1505
1501
  // https://go.mparticle.com/work/SQDSDKS-6357
1506
1502
  mpInstance._Store.context =
1507
1503
  identityApiResult.context || mpInstance._Store.context;
@@ -20,6 +20,10 @@ const DataPlanMatchType = {
20
20
  ProductImpression: "product_impression"
21
21
  }
22
22
 
23
+ const withNoChange = (event: SDKEvent): SDKEvent => event;
24
+ const withoutUserAttributes = (event: SDKEvent): SDKEvent => ({ ...event, UserAttributes: {} });
25
+ const withoutUserIdentities = (event: SDKEvent): SDKEvent => ({ ...event, UserIdentities: [] });
26
+
23
27
  /*
24
28
  inspiration from https://github.com/mParticle/data-planning-node/blob/master/src/data_planning/data_plan_event_validator.ts
25
29
  but modified to only include commerce events, custom events, screen views, and removes validation
@@ -318,29 +322,51 @@ export default class KitBlocker {
318
322
  then product attributes if applicable, then user attributes,
319
323
  then the user identities
320
324
  */
321
- try {
322
- if (event) {
323
- event = this.transformEventAndEventAttributes(event)
324
- }
325
-
326
- if (event && event.EventDataType === Types.MessageType.Commerce) {
327
- event = this.transformProductAttributes(event);
328
- }
329
-
330
- if (event) {
331
- event = this.transformUserAttributes(event);
332
- event = this.transformUserIdentities(event);
333
- }
334
-
335
- return event;
336
- } catch(e) {
325
+ if (event) {
326
+ event = this.applyBlockingStep('the event and its attributes', this.transformEventAndEventAttributes, event, withNoChange);
327
+ }
328
+
329
+ if (event && event.EventDataType === Types.MessageType.Commerce) {
330
+ event = this.applyBlockingStep('product attributes', this.transformProductAttributes, event, withNoChange);
331
+ }
332
+
333
+ if (event) {
334
+ event = this.applyBlockingStep('user attributes', this.transformUserAttributes, event, withoutUserAttributes);
335
+ event = this.applyBlockingStep('user identities', this.transformUserIdentities, event, withoutUserIdentities);
336
+ }
337
+
337
338
  return event;
338
- }
339
+ }
340
+
341
+ applyBlockingStep(
342
+ filteredData: string,
343
+ step: (event: SDKEvent) => SDKEvent,
344
+ event: SDKEvent,
345
+ onFailure: (event: SDKEvent) => SDKEvent
346
+ ): SDKEvent {
347
+ try {
348
+ return step.call(this, event);
349
+ } catch (stepError) {
350
+ const fallbackEvent = onFailure(event);
351
+ this.reportFailedStep(filteredData, stepError);
352
+ return fallbackEvent;
353
+ }
354
+ }
355
+
356
+ reportFailedStep(filteredData: string, stepError: unknown): void {
357
+ try {
358
+ this.mpInstance.Logger.error('Kit blocking could not filter ' + filteredData + ': ' + stepError);
359
+ } catch {
360
+ // Best-effort: a logger or error value that throws must not stop the remaining steps.
361
+ }
339
362
  }
340
363
 
341
364
  transformEventAndEventAttributes(event: SDKEvent): SDKEvent {
342
365
  const clonedEvent = {...event};
343
366
  const baseEvent: BaseEvent = convertEvent(clonedEvent);
367
+ if (!baseEvent) {
368
+ return clonedEvent;
369
+ }
344
370
  const matchKey: string = this.getMatchKey(baseEvent);
345
371
  const matchedEvent = this.dataPlanMatchLookups[matchKey];
346
372
 
@@ -445,17 +471,15 @@ export default class KitBlocker {
445
471
  transformUserAttributes(event: SDKEvent) {
446
472
  const clonedEvent = {...event};
447
473
  if (this.blockUserAttributes) {
448
- /*
449
- If the user attribute is not found in the matchedAttributes
450
- then remove it from event.UserAttributes as it is blocked
451
- */
452
474
  const matchedAttributes = this.dataPlanMatchLookups['user_attributes'];
453
- if (this.mpInstance._Helpers.isObject(matchedAttributes)) {
475
+ if (this.mpInstance._Helpers.isObject(matchedAttributes) && clonedEvent.UserAttributes) {
476
+ const plannedUserAttributes = {};
454
477
  for (const ua of Object.keys(clonedEvent.UserAttributes)) {
455
- if (!matchedAttributes[ua]) {
456
- delete clonedEvent.UserAttributes[ua]
478
+ if (matchedAttributes[ua] === true) {
479
+ plannedUserAttributes[ua] = clonedEvent.UserAttributes[ua];
457
480
  }
458
481
  }
482
+ clonedEvent.UserAttributes = plannedUserAttributes;
459
483
  }
460
484
  }
461
485
 
@@ -513,27 +537,18 @@ export default class KitBlocker {
513
537
  }
514
538
 
515
539
  transformUserIdentities(event: SDKEvent) {
516
- /*
517
- If the user identity is not found in matchedIdentities
518
- then remove it from event.UserIdentities as it is blocked.
519
- event.UserIdentities is of type [{Identity: 'id1', Type: 7}, ...]
520
- and so to compare properly in matchedIdentities, each Type needs
521
- to be converted to an identityName
522
- */
523
540
  const clonedEvent = {...event};
524
541
 
525
542
  if (this.blockUserIdentities) {
526
543
  const matchedIdentities = this.dataPlanMatchLookups['user_identities'];
527
544
  if (this.mpInstance._Helpers.isObject(matchedIdentities)) {
528
545
  if (clonedEvent?.UserIdentities?.length) {
529
- clonedEvent.UserIdentities.forEach((uiByType, i) => {
546
+ clonedEvent.UserIdentities = clonedEvent.UserIdentities.filter(uiByType => {
530
547
  const identityName = Types.IdentityType.getIdentityName(
531
548
  this.mpInstance._Helpers.parseNumber(uiByType.Type)
532
549
  );
533
-
534
- if (!matchedIdentities[identityName]) {
535
- clonedEvent.UserIdentities.splice(i, 1);
536
- }
550
+
551
+ return matchedIdentities[identityName];
537
552
  });
538
553
  }
539
554
  }
@@ -118,7 +118,7 @@ export default function NativeSdkHelpers(
118
118
  this.isBridgeV1Available = function(): boolean {
119
119
  if (
120
120
  mpInstance._Store.SDKConfig.useNativeSdk ||
121
- window.mParticleAndroid ||
121
+ getAndroidBridge(androidBridgeNameBase) ||
122
122
  mpInstance._Store.SDKConfig.isIOS
123
123
  ) {
124
124
  return true;
@@ -160,14 +160,13 @@ export default function NativeSdkHelpers(
160
160
  };
161
161
 
162
162
  this.sendViaBridgeV1 = function(path: string, value: string): void {
163
- if (
164
- window.mParticleAndroid &&
165
- window.mParticleAndroid.hasOwnProperty(path)
166
- ) {
163
+ const androidBridge = getAndroidBridge(androidBridgeNameBase);
164
+
165
+ if (androidBridge && androidBridge.hasOwnProperty(path)) {
167
166
  mpInstance.Logger.verbose(
168
167
  Messages.InformationMessages.SendAndroid + path
169
168
  );
170
- window.mParticleAndroid[path](value);
169
+ androidBridge[path](value);
171
170
  } else if (mpInstance._Store.SDKConfig.isIOS) {
172
171
  mpInstance.Logger.verbose(
173
172
  Messages.InformationMessages.SendIOS + path
@@ -1,5 +1,4 @@
1
1
  import {
2
- IdentityApiData,
3
2
  MPID,
4
3
  Product,
5
4
  UserIdentities,
@@ -111,7 +110,6 @@ export interface IPersistence {
111
110
  domain: string,
112
111
  maxCookieSize: number
113
112
  ): string;
114
- findPrevCookiesBasedOnUI(identityApiData: IdentityApiData): void;
115
113
  encodePersistence(persistence: string): string;
116
114
  decodePersistence(persistenceString: string | null): string | void;
117
115
  getCookieDomain(): string;
@@ -3,7 +3,7 @@ import Polyfill from './polyfill';
3
3
  import * as Utils from './utils';
4
4
  import { IMParticleWebSDKInstance } from './mp-instance';
5
5
  import { IPersistence, IPersistenceMinified } from './persistence.interfaces';
6
- import { IdentityApiData, MPID } from '@mparticle/web-sdk';
6
+ import { MPID } from '@mparticle/web-sdk';
7
7
  import { CookieSyncDates } from './cookieSyncManager';
8
8
  import { Dictionary } from './utils';
9
9
  import { IMParticleInstanceManager } from './sdkRuntimeModels';
@@ -85,9 +85,9 @@ export default function _Persistence(
85
85
  ) {
86
86
  if (!cookies) {
87
87
  self.storeDataInMemory(localStorageData);
88
- return;
88
+ return undefined;
89
89
  }
90
- const allData = mergeStorageSources(localStorageData, cookies);
90
+ const allData = localStorageData || cookies;
91
91
  self.storeDataInMemory(allData);
92
92
  self.expireCookies(mpInstance._Store.storageName);
93
93
  return allData;
@@ -122,21 +122,24 @@ export default function _Persistence(
122
122
  }
123
123
  }
124
124
 
125
- function clearCorruptStorage(): void {
125
+ function clearCorruptStorage(cookieWasLoaded: boolean): void {
126
126
  if (
127
127
  self.useLocalStorage() &&
128
128
  mpInstance._Store.isLocalStorageAvailable
129
129
  ) {
130
- localStorage.removeItem(mpInstance._Store.storageName);
130
+ if (!cookieWasLoaded) {
131
+ localStorage.removeItem(mpInstance._Store.storageName);
132
+ }
131
133
  return;
132
134
  }
133
135
  self.expireCookies(mpInstance._Store.storageName);
134
136
  }
135
137
 
136
138
  this.initializeStorage = function(): void {
139
+ let cookies: IPersistenceMinified | null = null;
137
140
  try {
138
141
  const localStorageData = self.getLocalStorage();
139
- const cookies = self.getCookie();
142
+ cookies = self.getCookie();
140
143
 
141
144
  // https://go.mparticle.com/work/SQDSDKS-6045
142
145
  setFirstRunFromExistingData(localStorageData, cookies);
@@ -156,7 +159,7 @@ export default function _Persistence(
156
159
  } catch (e) {
157
160
  // If cookies or local storage is corrupt, we want to remove it
158
161
  // so that in the future, initializeStorage will work
159
- clearCorruptStorage();
162
+ clearCorruptStorage(Boolean(cookies));
160
163
  mpInstance.Logger.error('Error initializing storage: ' + e);
161
164
  }
162
165
  };
@@ -417,10 +420,7 @@ export default function _Persistence(
417
420
  l,
418
421
  parts,
419
422
  name,
420
- cookie,
421
- result: string | Dictionary<string> | undefined = key
422
- ? undefined
423
- : {};
423
+ cookie;
424
424
 
425
425
  mpInstance.Logger.verbose(Messages.InformationMessages.CookieSearch);
426
426
 
@@ -443,25 +443,23 @@ export default function _Persistence(
443
443
  }
444
444
 
445
445
  if (key && key === name) {
446
- result = (mpInstance._Helpers as Dictionary).converted(cookie);
447
- break;
448
- }
446
+ const decodedPersistence = self.decodePersistence(
447
+ (mpInstance._Helpers as Dictionary).converted(cookie)
448
+ );
449
+ const persistence = decodedPersistence
450
+ ? JSON.parse(decodedPersistence)
451
+ : null;
449
452
 
450
- if (!key) {
451
- (result as Dictionary<string>)[name as string] = (
452
- mpInstance._Helpers as Dictionary
453
- ).converted(cookie);
453
+ if (mpInstance._Helpers.isObject(persistence)) {
454
+ mpInstance.Logger.verbose(
455
+ Messages.InformationMessages.CookieFound
456
+ );
457
+ return persistence;
458
+ }
454
459
  }
455
460
  }
456
461
 
457
- if (result) {
458
- mpInstance.Logger.verbose(Messages.InformationMessages.CookieFound);
459
- return JSON.parse(
460
- self.decodePersistence(result as string) as string
461
- );
462
- } else {
463
- return null;
464
- }
462
+ return null;
465
463
  };
466
464
 
467
465
  // https://go.mparticle.com/work/SQDSDKS-5022
@@ -746,85 +744,6 @@ export default function _Persistence(
746
744
  );
747
745
  }
748
746
 
749
- function cookieUiMatchesRequestedIdentity(
750
- cookieUIs,
751
- requestedIdentityType: string,
752
- requestedValue
753
- ): boolean {
754
- for (let cookieUIType in cookieUIs) {
755
- if (
756
- requestedIdentityType === cookieUIType &&
757
- requestedValue === cookieUIs[cookieUIType]
758
- ) {
759
- return true;
760
- }
761
- }
762
- return false;
763
- }
764
-
765
- function findMpidForRequestedIdentity(
766
- persistence: IPersistenceMinified,
767
- requestedIdentityType: string,
768
- requestedValue
769
- ) {
770
- let matchedUser;
771
- for (let key in persistence) {
772
- // any value in persistence that has an MPID key will be an MPID to search through
773
- // other keys on the cookie are currentSessionMPIDs and currentMPID which should not be searched
774
- if (
775
- !mpInstance._Helpers.isObject(persistence[key]) ||
776
- !persistence[key].mpid
777
- ) {
778
- continue;
779
- }
780
- if (
781
- cookieUiMatchesRequestedIdentity(
782
- persistence[key].ui,
783
- requestedIdentityType,
784
- requestedValue
785
- )
786
- ) {
787
- matchedUser = key;
788
- }
789
- }
790
- return matchedUser;
791
- }
792
-
793
- function findMpidMatchingIdentities(
794
- persistence: IPersistenceMinified | null,
795
- identityApiData: IdentityApiData
796
- ) {
797
- let matchedUser;
798
- for (let requestedIdentityType in identityApiData.userIdentities) {
799
- if (!persistence || !Object.keys(persistence).length) {
800
- continue;
801
- }
802
- const match = findMpidForRequestedIdentity(
803
- persistence,
804
- requestedIdentityType,
805
- identityApiData.userIdentities[requestedIdentityType]
806
- );
807
- if (match) {
808
- matchedUser = match;
809
- }
810
- }
811
- return matchedUser;
812
- }
813
-
814
- this.findPrevCookiesBasedOnUI = function(identityApiData: IdentityApiData): void {
815
- const persistence = mpInstance._Persistence.getPersistence();
816
- if (!identityApiData) {
817
- return;
818
- }
819
- const matchedUser = findMpidMatchingIdentities(
820
- persistence,
821
- identityApiData
822
- );
823
- if (matchedUser) {
824
- self.storeDataInMemory(persistence, matchedUser);
825
- }
826
- };
827
-
828
747
  function isNonEmptyArrayOrObject(value): boolean {
829
748
  if (Array.isArray(value)) {
830
749
  return value.length > 0;
@@ -947,8 +866,6 @@ export default function _Persistence(
947
866
  continue;
948
867
  }
949
868
  if (!SDKv2NonMPIDCookieKeys[mpid]) {
950
- // Written as an object by encodeMpidRecords, then read as one by
951
- // findMpidForRequestedIdentity and copied by copyNonCurrentUserMpids.
952
869
  if (!mpInstance._Helpers.isObject(persistence[mpid])) {
953
870
  delete persistence[mpid];
954
871
  continue;
@@ -26,6 +26,8 @@ import { subscribeToRouteChange } from "./routeChangeMonitor";
26
26
 
27
27
  const PASSBACK_CONVERSION_TRACKING_ID = 'passbackconversiontrackingid';
28
28
 
29
+ type AttributeTargetUser = Pick<IMParticleUser, 'getUserIdentities' | 'setUserAttributes'>;
30
+
29
31
  // https://docs.rokt.com/developers/integration-guides/web/library/attributes
30
32
  export type RoktAttributeValueArray = Array<string | number | boolean>;
31
33
  export type RoktAttributeValueType = string | number | boolean | undefined | null;
@@ -374,7 +376,11 @@ export default class RoktManager {
374
376
  });
375
377
  }
376
378
 
377
- if (!isEmpty(newIdentities)) {
379
+ const userAttributes = this.toUserAttributes(mappedAttributes);
380
+
381
+ if (isEmpty(newIdentities)) {
382
+ this.setUserAttributes(this.currentUser, userAttributes);
383
+ } else {
378
384
  // Fire-and-forget identify — best-effort, does not block selectPlacements
379
385
  try {
380
386
  this.identityService.identify(
@@ -391,6 +397,7 @@ export default class RoktManager {
391
397
  'Background identify failed with HTTP ' + httpCode
392
398
  );
393
399
  }
400
+ this.setUserAttributes(result?.getUser?.(), userAttributes, newIdentities);
394
401
  // Drain any selectPlacements calls that were deferred while
395
402
  // identify was in-flight. By the time this callback fires,
396
403
  // identityCallInFlight has already been reset to false.
@@ -402,6 +409,7 @@ export default class RoktManager {
402
409
  'Background identify threw an error: ' +
403
410
  getErrorMessage(error)
404
411
  );
412
+ this.setUserAttributes(this.identityService.getCurrentUser(), userAttributes, newIdentities);
405
413
  }
406
414
  }
407
415
 
@@ -411,8 +419,6 @@ export default class RoktManager {
411
419
  // so stale identity state does not persist beyond this invocation.
412
420
  const finalUserIdentities = { ...currentUserIdentities, ...newIdentities };
413
421
 
414
- this.setUserAttributes(mappedAttributes);
415
-
416
422
  const timeOnSite = this.store?.getTimeOnSite?.();
417
423
  const totalTimeOnSite = this.store?.getTotalTimeOnSite?.();
418
424
 
@@ -641,7 +647,7 @@ export default class RoktManager {
641
647
  return Boolean(this.kit && this.kit.launcher);
642
648
  }
643
649
 
644
- private setUserAttributes(attributes: RoktAttributes): void {
650
+ private toUserAttributes(attributes: RoktAttributes): Dictionary<RoktAttributeValueType> {
645
651
  const reservedAttributes = ['sandbox'];
646
652
  const filteredAttributes = {};
647
653
 
@@ -652,13 +658,39 @@ export default class RoktManager {
652
658
  }
653
659
  }
654
660
 
661
+ return filteredAttributes;
662
+ }
663
+
664
+ private setUserAttributes(
665
+ user: AttributeTargetUser | null | undefined,
666
+ userAttributes: Dictionary<RoktAttributeValueType>,
667
+ identitiesUserMustNotContradict: UserIdentities = {}
668
+ ): void {
669
+ if (!user) {
670
+ this.logger.warning('selectPlacements attributes were not set because there is no user to set them on.');
671
+ return;
672
+ }
673
+
655
674
  try {
656
- this.currentUser.setUserAttributes(filteredAttributes);
675
+ if (this.hasContradictingIdentity(user, identitiesUserMustNotContradict)) {
676
+ this.logger.warning(
677
+ 'selectPlacements attributes were not set because identify did not resolve to a user with the passed email or hashed email.'
678
+ );
679
+ return;
680
+ }
681
+ user.setUserAttributes(userAttributes);
657
682
  } catch (error) {
658
683
  this.logger.error('Error setting user attributes: ' + error);
659
684
  }
660
685
  }
661
686
 
687
+ private hasContradictingIdentity(user: AttributeTargetUser, identities: UserIdentities): boolean {
688
+ return Object.keys(identities).some((identityType) => {
689
+ const userValue = user.getUserIdentities()?.userIdentities?.[identityType];
690
+ return Boolean(userValue) && userValue !== identities[identityType];
691
+ });
692
+ }
693
+
662
694
  private mapPlacementAttributes(attributes: RoktAttributes, placementAttributesMapping: Dictionary<string>[]): RoktAttributes {
663
695
  const mappingLookup: { [key: string]: string } = {};
664
696
  for (const mapping of placementAttributesMapping) {