@mparticle/web-sdk 2.58.0 → 2.59.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mparticle/web-sdk",
3
- "version": "2.58.0",
3
+ "version": "2.59.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
package/src/apiClient.ts CHANGED
@@ -10,6 +10,7 @@ import { IMParticleUser, ISDKUserAttributes } from './identity-user-interfaces';
10
10
  import { AsyncUploader, FetchUploader, XHRUploader } from './uploaders';
11
11
  import { IMParticleWebSDKInstance } from './mp-instance';
12
12
  import { appendUserInfo } from './user-utils';
13
+ import { hasExplicitIdentifier } from './identity-utils';
13
14
 
14
15
  export interface IAPIClient {
15
16
  uploader: BatchUploader | null;
@@ -88,6 +89,35 @@ export default function APIClient(
88
89
  });
89
90
  };
90
91
 
92
+ // When noFunctional is set and there are no identities passed, the SDK will not fully initialize.
93
+ // In this case, there will be no MPID, but we still want kits to initialize and forward the event to kits.
94
+ // The original event is queued for the MP server upload path so it can be sent once an MPID is returned.
95
+ // Returns true if the event was handled by this path (caller should return early).
96
+ const handleNoFunctionalPreMpidEvent = (event: SDKEvent, mpid: string | undefined): boolean => {
97
+ const noFunctionalWithoutId =
98
+ mpInstance._CookieConsentManager?.getNoFunctional() &&
99
+ !hasExplicitIdentifier(mpInstance._Store);
100
+
101
+ if (!noFunctionalWithoutId || mpid || !mpInstance._Store.configurationLoaded || mpInstance._Store.requireDelay) {
102
+ return false;
103
+ }
104
+
105
+ let forwarderEvent = event;
106
+ if (kitBlocker?.kitBlockingEnabled) {
107
+ forwarderEvent = kitBlocker.createBlockedEvent(event);
108
+ }
109
+ if (forwarderEvent) {
110
+ mpInstance._Forwarders.sendEventToForwarders(forwarderEvent);
111
+ event.AlreadySentToForwarders = true;
112
+ }
113
+ mpInstance.Logger.verbose(
114
+ 'noFunctional event forwarded to kits and queued for MP server upload when MPID is available.'
115
+ );
116
+ mpInstance._Store.eventQueue.push(event);
117
+
118
+ return true;
119
+ };
120
+
91
121
  this.sendEventToServer = function(event, _options) {
92
122
  const defaultOptions = {
93
123
  shouldUploadEvent: true,
@@ -112,8 +142,14 @@ export default function APIClient(
112
142
  mpInstance._Store.integrationDelayTimeoutStart,
113
143
  Date.now()
114
144
  );
115
- // We queue events if there is no MPID (MPID is null, or === 0), or there are integrations that that require this to stall because integration attributes
116
- // need to be set, or if we are still fetching the config (self hosted only), and so require delaying events
145
+
146
+ if (handleNoFunctionalPreMpidEvent(event, mpid)) {
147
+ return;
148
+ }
149
+
150
+ // We queue events if there is no MPID (MPID is null, or === 0), or there are integrations that
151
+ // require this to stall because integration attributes need to be set, or if we are still
152
+ // fetching the config (self hosted only), and so require delaying events
117
153
  if (
118
154
  !mpid ||
119
155
  mpInstance._Store.requireDelay ||
@@ -146,8 +182,10 @@ export default function APIClient(
146
182
  }
147
183
 
148
184
  // We need to check event again, because kitblocking
149
- // can nullify the event
150
- if (event) {
185
+ // can nullify the event.
186
+ // Skip if forwarders were already called in the noFunctional pre-MPID path
187
+ // to prevent double-sending when the event queue is later flushed.
188
+ if (event && !event.AlreadySentToForwarders) {
151
189
  mpInstance._Forwarders.sendEventToForwarders(event);
152
190
  }
153
191
  }
@@ -74,7 +74,10 @@ export class BatchUploader {
74
74
  // so that we don't have to check it every time
75
75
  this.offlineStorageEnabled = this.isOfflineStorageAvailable();
76
76
 
77
- if (this.offlineStorageEnabled) {
77
+ // When noFunctional is true, prevent events/batches storage
78
+ const noFunctional = mpInstance._CookieConsentManager?.getNoFunctional();
79
+
80
+ if (this.offlineStorageEnabled && !noFunctional) {
78
81
  this.eventVault = new SessionStorageVault<SDKEvent[]>(
79
82
  `${mpInstance._Store.storageName}-events`,
80
83
  {
@@ -73,6 +73,12 @@ export default function CookieSyncManager(
73
73
  return;
74
74
  }
75
75
 
76
+ // When noFunctional is true, persistence is not saved, so we cannot track cookie sync
77
+ // dates. Skip cookie sync to avoid running it on every page load.
78
+ if (mpInstance._CookieConsentManager?.getNoFunctional()) {
79
+ return;
80
+ }
81
+
76
82
  const persistence = mpInstance._Persistence.getPersistence();
77
83
 
78
84
  if (isEmpty(persistence)) {
@@ -8,10 +8,12 @@ export default class ForegroundTimeTracker {
8
8
  public startTime: number = 0;
9
9
  public totalTime: number = 0;
10
10
 
11
- constructor(timerKey: string) {
11
+ constructor(timerKey: string, private noFunctional: boolean = false) {
12
12
  this.localStorageName = `mprtcl-tos-${timerKey}`;
13
13
  this.timerVault = new LocalStorageVault<number>(this.localStorageName);
14
- this.loadTimeFromStorage();
14
+ if (!this.noFunctional) {
15
+ this.loadTimeFromStorage();
16
+ }
15
17
  this.addHandlers();
16
18
  if (document.hidden === false) {
17
19
  this.startTracking();
@@ -63,7 +65,7 @@ export default class ForegroundTimeTracker {
63
65
  }
64
66
 
65
67
  public updateTimeInPersistence(): void {
66
- if (this.isTrackerActive) {
68
+ if (this.isTrackerActive && !this.noFunctional) {
67
69
  this.timerVault.store(Math.round(this.totalTime));
68
70
  }
69
71
  }
@@ -1,5 +1,5 @@
1
1
  import Constants, { ONE_DAY_IN_SECONDS, MILLIS_IN_ONE_SEC } from './constants';
2
- import { Dictionary, parseNumber, isObject, generateHash } from './utils';
2
+ import { Dictionary, parseNumber, isObject, generateHash, isEmpty } from './utils';
3
3
  import { BaseVault } from './vault';
4
4
  import Types from './types';
5
5
  import {
@@ -13,6 +13,7 @@ import {
13
13
  IIdentityResponse,
14
14
  IMParticleUser,
15
15
  } from './identity-user-interfaces';
16
+ import { IStore } from './store';
16
17
 
17
18
  const { Identify, Modify, Login, Logout } = Constants.IdentityMethods;
18
19
  export const CACHE_HEADER = 'x-mp-max-age' as const;
@@ -298,3 +299,25 @@ export const hasIdentityRequestChanged = (
298
299
  JSON.stringify(currentUserIdentities) !== JSON.stringify(newIdentities)
299
300
  );
300
301
  };
302
+
303
+ /**
304
+ * Checks if deviceId or other user identifiers (like email) were explicitly provided
305
+ * by the partner via config.deviceId or config.identifyRequest.userIdentities.
306
+ * When noFunctional is true, then cookies are blocked, so the partner must explicitly
307
+ * pass deviceId or other identifiers to prevent new users from being created on each page load.
308
+ *
309
+ * @param store - The SDK store (provides SDKConfig.deviceId and SDKConfig.identifyRequest.userIdentities)
310
+ * @returns true if deviceId or other identifiers were explicitly provided in config, false otherwise
311
+ */
312
+ export const hasExplicitIdentifier = (store: IStore | undefined | null): boolean => {
313
+ const userIdentities = store?.SDKConfig?.identifyRequest?.userIdentities;
314
+ if (
315
+ userIdentities &&
316
+ isObject(userIdentities) &&
317
+ !isEmpty(userIdentities) &&
318
+ Object.values(userIdentities).some(Boolean)
319
+ ) {
320
+ return true;
321
+ }
322
+ return !!store?.SDKConfig?.deviceId;
323
+ };
@@ -37,8 +37,8 @@ import KitBlocker from './kitBlocking';
37
37
  import ConfigAPIClient, { IKitConfigs } from './configAPIClient';
38
38
  import IdentityAPIClient from './identityApiClient';
39
39
  import { isFunction, parseConfig, valueof, generateDeprecationMessage } from './utils';
40
- import { LocalStorageVault } from './vault';
41
- import { removeExpiredIdentityCacheDates } from './identity-utils';
40
+ import { DisabledVault, LocalStorageVault } from './vault';
41
+ import { removeExpiredIdentityCacheDates, hasExplicitIdentifier } from './identity-utils';
42
42
  import IntegrationCapture from './integrationCapture';
43
43
  import { IPreInit, processReadyQueue } from './pre-init-utils';
44
44
  import { BaseEvent, MParticleWebSDK, SDKHelpersApi } from './sdkRuntimeModels';
@@ -97,6 +97,7 @@ export interface IMParticleWebSDKInstance extends MParticleWebSDK {
97
97
  getLauncherInstanceGuid: () => string;
98
98
  captureTiming(metricName: string);
99
99
  processQueueOnIdentityFailure?: () => void;
100
+ processQueueOnNoFunctional?: () => void;
100
101
  }
101
102
 
102
103
  const { Messages, HTTPCodes, FeatureFlags, CaptureIntegrationSpecificIdsV2Modes } = Constants;
@@ -150,13 +151,30 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
150
151
  if (self._Store?.isInitialized) {
151
152
  return;
152
153
  }
153
-
154
+
154
155
  if (self._Store?.identityCallFailed && self._RoktManager?.isReady()) {
155
156
  self._RoktManager.processMessageQueue();
156
157
  self._preInit.readyQueue = processReadyQueue(self._preInit.readyQueue);
157
158
  }
158
159
  };
159
160
 
161
+ /**
162
+ * Drains the ready queue for noFunctional sessions with no explicit identity.
163
+ */
164
+ this.processQueueOnNoFunctional = function() {
165
+ if (self._Store?.isInitialized) {
166
+ return;
167
+ }
168
+
169
+ const noFunctionalWithoutId =
170
+ self._CookieConsentManager?.getNoFunctional() &&
171
+ !hasExplicitIdentifier(self._Store);
172
+
173
+ if (noFunctionalWithoutId) {
174
+ self._preInit.readyQueue = processReadyQueue(self._preInit.readyQueue);
175
+ }
176
+ };
177
+
160
178
  // required for forwarders once they reference the mparticle instance
161
179
  this.IdentityType = IdentityType;
162
180
  this.EventType = EventType;
@@ -280,11 +298,16 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
280
298
  * @param {Function} f Callback to execute
281
299
  */
282
300
  this.ready = function(f) {
301
+ const noFunctionalWithoutId =
302
+ self._CookieConsentManager?.getNoFunctional() &&
303
+ !hasExplicitIdentifier(self._Store);
304
+
283
305
  const shouldExecute = isFunction(f) && (
284
- self._Store?.isInitialized ||
285
- (self._Store?.identityCallFailed && self._RoktManager.isReady())
306
+ self._Store?.isInitialized ||
307
+ (self._Store?.identityCallFailed && self._RoktManager.isReady()) ||
308
+ noFunctionalWithoutId
286
309
  );
287
-
310
+
288
311
  if (shouldExecute) {
289
312
  f();
290
313
  } else {
@@ -1135,11 +1158,16 @@ export default function mParticleInstance(this: IMParticleWebSDKInstance, instan
1135
1158
  * @param {String or Number} value value for session attribute
1136
1159
  */
1137
1160
  this.setSessionAttribute = function(key, value) {
1138
- const queued = queueIfNotInitialized(function() {
1139
- self.setSessionAttribute(key, value);
1140
- }, self);
1161
+ const skipQueue =
1162
+ self._CookieConsentManager?.getNoFunctional() &&
1163
+ !hasExplicitIdentifier(self._Store);
1141
1164
 
1142
- if (queued) return;
1165
+ if (!skipQueue) {
1166
+ const queued = queueIfNotInitialized(function() {
1167
+ self.setSessionAttribute(key, value);
1168
+ }, self);
1169
+ if (queued) return;
1170
+ }
1143
1171
 
1144
1172
  // Logs to cookie
1145
1173
  // And logs to in-memory object
@@ -1512,6 +1540,10 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1512
1540
  );
1513
1541
  }
1514
1542
 
1543
+ // For noFunctional sessions with no identity, drain any pre-init ready callbacks
1544
+ // that were queued before _CookieConsentManager was available to evaluate the condition.
1545
+ mpInstance.processQueueOnNoFunctional?.();
1546
+
1515
1547
  // https://go.mparticle.com/work/SQDSDKS-6040
1516
1548
  if (mpInstance._Store.isFirstRun) {
1517
1549
  mpInstance._Store.isFirstRun = false;
@@ -1584,6 +1616,14 @@ function createKitBlocker(config, mpInstance) {
1584
1616
  }
1585
1617
 
1586
1618
  function createIdentityCache(mpInstance) {
1619
+ // Identity expects mpInstance._Identity.idCache to always exist. DisabledVault
1620
+ // ensures no identity response data is written to localStorage when noFunctional is true
1621
+ if (mpInstance._CookieConsentManager?.getNoFunctional()) {
1622
+ return new DisabledVault(`${mpInstance._Store.storageName}-id-cache`, {
1623
+ logger: mpInstance.Logger,
1624
+ });
1625
+ }
1626
+
1587
1627
  return new LocalStorageVault(`${mpInstance._Store.storageName}-id-cache`, {
1588
1628
  logger: mpInstance.Logger,
1589
1629
  });
@@ -1664,15 +1704,21 @@ function processIdentityCallback(
1664
1704
  }
1665
1705
 
1666
1706
  function queueIfNotInitialized(func, self) {
1667
- // Core SDK methods must wait for Store initialization
1668
- if (!self._Store?.isInitialized) {
1669
- self._preInit.readyQueue.push(function() {
1670
- if (self._Store?.isInitialized) {
1671
- func();
1672
- }
1673
- });
1674
- return true;
1707
+ // When noFunctional is true with no explicit identifier, the SDK will never
1708
+ // receive an MPID. Let these calls through so events can still reach forwarders immediately.
1709
+ // sendEventToServer handles queuing for the MP server upload path separately.
1710
+ const noFunctionalWithoutId =
1711
+ self._CookieConsentManager?.getNoFunctional() &&
1712
+ !hasExplicitIdentifier(self._Store);
1713
+
1714
+ if (self._Store?.isInitialized || noFunctionalWithoutId) {
1715
+ return false;
1675
1716
  }
1676
- return false;
1717
+ self._preInit.readyQueue.push(function() {
1718
+ if (self._Store?.isInitialized) {
1719
+ func();
1720
+ }
1721
+ });
1722
+ return true;
1677
1723
  }
1678
1724
 
@@ -230,6 +230,11 @@ export default function _Persistence(mpInstance) {
230
230
  return;
231
231
  }
232
232
 
233
+ // Block mprtcl-v4 localStorage when noFunctional is true
234
+ if (mpInstance._CookieConsentManager?.getNoFunctional()) {
235
+ return;
236
+ }
237
+
233
238
  var key = mpInstance._Store.storageName,
234
239
  localStorageData = self.getLocalStorage() || {},
235
240
  currentUser = mpInstance.Identity.getCurrentUser(),
@@ -398,6 +403,11 @@ export default function _Persistence(mpInstance) {
398
403
  // https://go.mparticle.com/work/SQDSDKS-5022
399
404
  // https://go.mparticle.com/work/SQDSDKS-6021
400
405
  this.setCookie = function() {
406
+ // Block mprtcl-v4 cookies when noFunctional is true
407
+ if (mpInstance._CookieConsentManager?.getNoFunctional()) {
408
+ return;
409
+ }
410
+
401
411
  var mpid,
402
412
  currentUser = mpInstance.Identity.getCurrentUser();
403
413
  if (currentUser) {
@@ -803,6 +813,11 @@ export default function _Persistence(mpInstance) {
803
813
 
804
814
  // https://go.mparticle.com/work/SQDSDKS-6021
805
815
  this.savePersistence = function(persistence) {
816
+ // Block mprtcl-v4 persistence when noFunctional is true
817
+ if (mpInstance._CookieConsentManager?.getNoFunctional()) {
818
+ return;
819
+ }
820
+
806
821
  var encodedPersistence = self.encodePersistence(
807
822
  JSON.stringify(persistence)
808
823
  ),
@@ -94,6 +94,7 @@ export interface SDKEvent {
94
94
  ExpandedEventCount: number;
95
95
  ActiveTimeOnSite: number;
96
96
  IsBackgroundAST?: boolean;
97
+ AlreadySentToForwarders?: boolean;
97
98
  }
98
99
 
99
100
  export interface SDKGeoLocation {
@@ -5,7 +5,7 @@ import Types from './types';
5
5
  import { generateDeprecationMessage } from './utils';
6
6
  import { IMParticleUser } from './identity-user-interfaces';
7
7
  import { IMParticleWebSDKInstance } from './mp-instance';
8
- import { hasIdentityRequestChanged } from './identity-utils';
8
+ import { hasIdentityRequestChanged, hasExplicitIdentifier } from './identity-utils';
9
9
 
10
10
  const { Messages } = Constants;
11
11
 
@@ -45,7 +45,12 @@ export default function SessionManager(
45
45
  const currentUser = mpInstance.Identity.getCurrentUser();
46
46
  const sdkIdentityRequest = SDKConfig.identifyRequest;
47
47
 
48
+ const shouldSuppressIdentify =
49
+ mpInstance._CookieConsentManager?.getNoFunctional() &&
50
+ !hasExplicitIdentifier(mpInstance._Store);
51
+
48
52
  if (
53
+ !shouldSuppressIdentify &&
49
54
  hasIdentityRequestChanged(currentUser, sdkIdentityRequest)
50
55
  ) {
51
56
  mpInstance.Identity.identify(
@@ -102,7 +107,11 @@ export default function SessionManager(
102
107
 
103
108
  self.setSessionTimer();
104
109
 
105
- if (!mpInstance._Store.identifyCalled) {
110
+ const shouldSuppressIdentify =
111
+ mpInstance._CookieConsentManager?.getNoFunctional() &&
112
+ !hasExplicitIdentifier(mpInstance._Store);
113
+
114
+ if (!mpInstance._Store.identifyCalled && !shouldSuppressIdentify) {
106
115
  mpInstance.Identity.identify(
107
116
  mpInstance._Store.SDKConfig.identifyRequest,
108
117
  mpInstance._Store.SDKConfig.identityCallback
package/src/store.ts CHANGED
@@ -732,7 +732,8 @@ export default function Store(
732
732
 
733
733
  if (workspaceToken) {
734
734
  this.SDKConfig.workspaceToken = workspaceToken;
735
- mpInstance._timeOnSiteTimer = new ForegroundTimer(workspaceToken);
735
+ const noFunctional = config?.launcherOptions?.noFunctional === true;
736
+ mpInstance._timeOnSiteTimer = new ForegroundTimer(workspaceToken, noFunctional);
736
737
  } else {
737
738
  mpInstance.Logger.warning(
738
739
  'You should have a workspaceToken on your config object for security purposes.'
package/src/vault.ts CHANGED
@@ -102,4 +102,25 @@ export class SessionStorageVault<StorableItem> extends BaseVault<StorableItem> {
102
102
  constructor(storageKey: string, options?: IVaultOptions) {
103
103
  super(storageKey, window.sessionStorage, options);
104
104
  }
105
+ }
106
+
107
+ // DisabledVault is used when persistence is disabled by privacy flags.
108
+ export class DisabledVault<StorableItem> extends BaseVault<StorableItem> {
109
+ constructor(storageKey: string, options?: IVaultOptions) {
110
+ super(storageKey, window.localStorage, options);
111
+ this.contents = null;
112
+ this.storageObject.removeItem(this._storageKey);
113
+ }
114
+
115
+ public store(_item: StorableItem): void {
116
+ this.contents = null;
117
+ }
118
+
119
+ public retrieve(): StorableItem | null {
120
+ return this.contents;
121
+ }
122
+
123
+ public purge(): void {
124
+ this.contents = null;
125
+ }
105
126
  }