@mparticle/web-sdk 3.6.2 → 3.7.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": "3.6.2",
3
+ "version": "3.7.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
package/src/consent.ts CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  GDPRConsentState,
5
5
  PrivacyConsentState,
6
6
  } from '@mparticle/web-sdk';
7
- import { Dictionary, isObject } from './utils';
7
+ import { Dictionary, hasOwnProp, isObject } from './utils';
8
8
  import KitFilterHelper from './kitFilterHelper';
9
9
  import Constants from './constants';
10
10
  import { IMParticleUser } from './identity-user-interfaces';
@@ -287,7 +287,7 @@ export default function Consent(this: IConsent, mpInstance: IMParticleWebSDKInst
287
287
  const state: ConsentState = self.createConsentState();
288
288
  if (json.gdpr) {
289
289
  for (const purpose in json.gdpr) {
290
- if (json.gdpr.hasOwnProperty(purpose)) {
290
+ if (hasOwnProp(json.gdpr, purpose)) {
291
291
  const gdprConsent = self.createPrivacyConsent(
292
292
  json.gdpr[purpose].c,
293
293
  json.gdpr[purpose].ts,
@@ -301,7 +301,7 @@ export default function Consent(this: IConsent, mpInstance: IMParticleWebSDKInst
301
301
  }
302
302
 
303
303
  if (json.ccpa) {
304
- if (json.ccpa.hasOwnProperty(CCPAPurpose)) {
304
+ if (hasOwnProp(json.ccpa, CCPAPurpose)) {
305
305
  const ccpaConsent = self.createPrivacyConsent(
306
306
  json.ccpa[CCPAPurpose].c,
307
307
  json.ccpa[CCPAPurpose].ts,
@@ -1,5 +1,6 @@
1
1
  import { NO_TARGETING_ATTRIBUTE } from './constants';
2
2
  import { IMParticleUser } from './identity-user-interfaces';
3
+ import { hasOwnProp } from './utils';
3
4
 
4
5
  /**
5
6
  * Cookie consent flags control SDK behavior based on user consent preferences for Rokt integration.
@@ -77,9 +78,7 @@ export default class CookieConsentManager implements ICookieConsentManager {
77
78
  if (this.flags.noTargeting) {
78
79
  user.setUserAttribute(NO_TARGETING_ATTRIBUTE, true);
79
80
  } else if (
80
- user
81
- .getAllUserAttributes()
82
- .hasOwnProperty(NO_TARGETING_ATTRIBUTE)
81
+ hasOwnProp(user.getAllUserAttributes(), NO_TARGETING_ATTRIBUTE)
83
82
  ) {
84
83
  user.removeUserAttribute(NO_TARGETING_ATTRIBUTE);
85
84
  }
@@ -87,76 +87,89 @@ export default function CookieSyncManager(
87
87
  }
88
88
 
89
89
  pixelConfigurations.forEach((pixelSettings: IPixelConfiguration) => {
90
- // set requiresConsent to false to start each additional pixel configuration
91
- // set to true only if filteringConsenRuleValues.values.length exists
92
- let requiresConsent = false;
93
- // Filtering rules as defined in UI
94
- const {
95
- filteringConsentRuleValues,
96
- pixelUrl,
97
- redirectUrl,
98
- moduleId,
99
- settings,
100
- // Tells you how often we should do a cookie sync (in days)
101
- frequencyCap,
102
- } = pixelSettings;
103
- const { values } = filteringConsentRuleValues || {};
104
-
105
- if (isEmpty(pixelUrl)) {
106
- return;
90
+ try {
91
+ // set requiresConsent to false to start each additional pixel configuration
92
+ // set to true only if filteringConsenRuleValues.values.length exists
93
+ let requiresConsent = false;
94
+ // Filtering rules as defined in UI
95
+ const {
96
+ filteringConsentRuleValues,
97
+ pixelUrl,
98
+ redirectUrl,
99
+ moduleId,
100
+ settings,
101
+ // Tells you how often we should do a cookie sync (in days)
102
+ frequencyCap,
103
+ } = pixelSettings;
104
+ const { values } = filteringConsentRuleValues || {};
105
+
106
+ if (isEmpty(pixelUrl)) {
107
+ return;
108
+ }
109
+
110
+ if (!isEmpty(values)) {
111
+ requiresConsent = true;
112
+ }
113
+
114
+ // If MPID is new to cookies, we should not try to perform the cookie sync
115
+ // because a cookie sync can only occur once a user either consents or doesn't.
116
+ // we should not check if it's enabled if the user has a blank consent
117
+ if (requiresConsent && mpidIsNotInCookies) {
118
+ return;
119
+ }
120
+
121
+ // For Rokt, block cookie sync when noTargeting privacy flag is true
122
+ if (moduleId === PARTNER_MODULE_IDS.Rokt && mpInstance._CookieConsentManager.getNoTargeting()) {
123
+ return;
124
+ }
125
+
126
+ const { isEnabledForUserConsent } = mpInstance._Consent;
127
+
128
+ if (!isEnabledForUserConsent(filteringConsentRuleValues, mpInstance.Identity.getCurrentUser())) {
129
+ return;
130
+ }
131
+
132
+ const cookieSyncDates: CookieSyncDates = persistence[mpid]?.csd ?? {};
133
+ const lastSyncDateForModule: number = cookieSyncDates[moduleId] || null;
134
+
135
+ if (!isLastSyncDateExpired(frequencyCap, lastSyncDateForModule)) {
136
+ return;
137
+ }
138
+
139
+ // The Trade Desk requires a URL parameter for GDPR enabled users.
140
+ // It is optional but to simplify the code, we add it for all Trade
141
+ // // Desk cookie syncs.
142
+ const domain = moduleId === PARTNER_MODULE_IDS.TradeDesk ? window.location.hostname : undefined;
143
+
144
+ // Google Marketing Platform accepts a web-safe base64-encoded MPID via the
145
+ // `google_hm` query parameter, but only for accounts provisioned for Google
146
+ // Hosted Matching — Google returns errors otherwise. It is therefore gated
147
+ // behind the opt-in `enableHmTag` setting (off by default), which the server
148
+ // delivers as the string 'True' when the UI toggle is enabled.
149
+ const enableHmTag = settings?.enableHmTag?.toLowerCase() === 'true';
150
+ const isDoubleClickModule = moduleId === PARTNER_MODULE_IDS.DoubleclickDFP;
151
+ const usesHostedMatching = isDoubleClickModule && enableHmTag;
152
+ const base64Mpid = usesHostedMatching ? toWebSafeBase64(mpid) : undefined;
153
+
154
+ if (usesHostedMatching && !base64Mpid) {
155
+ mpInstance.Logger.warning(
156
+ 'Unable to base64 encode the current MPID; omitting the google_hm parameter from this cookie sync'
157
+ );
158
+ }
159
+
160
+ const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl, domain, base64Mpid);
161
+
162
+ self.performCookieSync(
163
+ fullUrl,
164
+ moduleId.toString(),
165
+ mpid,
166
+ cookieSyncDates
167
+ );
168
+ } catch (e) {
169
+ mpInstance.Logger.error(
170
+ 'Error performing cookie sync: ' + e
171
+ );
107
172
  }
108
-
109
- if (!isEmpty(values)) {
110
- requiresConsent = true;
111
- }
112
-
113
- // If MPID is new to cookies, we should not try to perform the cookie sync
114
- // because a cookie sync can only occur once a user either consents or doesn't.
115
- // we should not check if it's enabled if the user has a blank consent
116
- if (requiresConsent && mpidIsNotInCookies) {
117
- return;
118
- }
119
-
120
- // For Rokt, block cookie sync when noTargeting privacy flag is true
121
- if (moduleId === PARTNER_MODULE_IDS.Rokt && mpInstance._CookieConsentManager.getNoTargeting()) {
122
- return;
123
- }
124
-
125
- const { isEnabledForUserConsent } = mpInstance._Consent;
126
-
127
- if (!isEnabledForUserConsent(filteringConsentRuleValues, mpInstance.Identity.getCurrentUser())) {
128
- return;
129
- }
130
-
131
- const cookieSyncDates: CookieSyncDates = persistence[mpid]?.csd ?? {};
132
- const lastSyncDateForModule: number = cookieSyncDates[moduleId] || null;
133
-
134
- if (!isLastSyncDateExpired(frequencyCap, lastSyncDateForModule)) {
135
- return;
136
- }
137
-
138
- // The Trade Desk requires a URL parameter for GDPR enabled users.
139
- // It is optional but to simplify the code, we add it for all Trade
140
- // // Desk cookie syncs.
141
- const domain = moduleId === PARTNER_MODULE_IDS.TradeDesk ? window.location.hostname : undefined;
142
-
143
- // Google Marketing Platform accepts a web-safe base64-encoded MPID via the
144
- // `google_hm` query parameter, but only for accounts provisioned for Google
145
- // Hosted Matching — Google returns errors otherwise. It is therefore gated
146
- // behind the opt-in `enableHmTag` setting (off by default), which the server
147
- // delivers as the string 'True' when the UI toggle is enabled.
148
- const enableHmTag = settings?.enableHmTag?.toLowerCase() === 'true';
149
- const isDoubleClickModule = moduleId === PARTNER_MODULE_IDS.DoubleclickDFP;
150
- const base64Mpid = isDoubleClickModule && enableHmTag ? toWebSafeBase64(mpid) : undefined;
151
-
152
- const fullUrl = createCookieSyncUrl(mpid, pixelUrl, redirectUrl, domain, base64Mpid);
153
-
154
- self.performCookieSync(
155
- fullUrl,
156
- moduleId.toString(),
157
- mpid,
158
- cookieSyncDates
159
- );
160
173
  });
161
174
  };
162
175
 
@@ -12,6 +12,7 @@ import {
12
12
  SDKProduct,
13
13
  SDKPromotion,
14
14
  } from './sdkRuntimeModels';
15
+ import { IPageViewOptions } from './pageViewTracker';
15
16
  import { valueof } from './utils';
16
17
  import { EventType, ProductActionType, PromotionActionType } from './types';
17
18
 
@@ -40,7 +41,7 @@ export interface IEvents {
40
41
  eventOptions?: SDKEventOptions
41
42
  ): void;
42
43
  logOptOut(): void;
43
- logPageView(): void;
44
+ logPageView(options?: IPageViewOptions): void;
44
45
  logProductActionEvent(
45
46
  productActionType: valueof<typeof ProductActionType>,
46
47
  product: SDKProduct | SDKProduct[],
package/src/events.ts CHANGED
@@ -21,7 +21,11 @@ import {
21
21
  TransactionAttributes,
22
22
  } from '@mparticle/web-sdk';
23
23
  import { getHref, valueof } from './utils';
24
- import { allowedQueryParams } from './pageViewTracker';
24
+ import {
25
+ allowedQueryParams,
26
+ autoPageViewAttribute,
27
+ IPageViewOptions,
28
+ } from './pageViewTracker';
25
29
 
26
30
  interface DOMHandlerElement extends HTMLElement {
27
31
  href?: string;
@@ -149,7 +153,7 @@ export default function Events(
149
153
  // come from PageViewTracker instead, so both emitters attach the same
150
154
  // allowlisted query params — and this is the one that matters for campaign
151
155
  // attribution, since utm_*/gclid live on the entry URL.
152
- this.logPageView = function(): void {
156
+ this.logPageView = function(options?: IPageViewOptions): void {
153
157
  self.logEvent({
154
158
  messageType: Types.MessageType.PageView,
155
159
  name: 'PageView',
@@ -159,6 +163,7 @@ export default function Events(
159
163
  ...allowedQueryParams(getHref()),
160
164
  hostname: window.location.hostname,
161
165
  title: window.document.title,
166
+ ...autoPageViewAttribute(options && options.isAutoPageView),
162
167
  },
163
168
  eventType: Types.EventType.Unknown,
164
169
  });
@@ -1,7 +1,7 @@
1
- import Types from './types';
1
+ import Types, { getIdentityTypeFromStoredKey } from './types';
2
2
  import { IMParticleWebSDKInstance } from './mp-instance';
3
3
  import { MPID, UserIdentities } from '@mparticle/web-sdk';
4
- import { Dictionary } from './utils';
4
+ import { Dictionary, hasOwnProp, isReservedStoredPropertyName } from './utils';
5
5
  import KitBlocker from './kitBlocking';
6
6
  import { MPForwarder } from './forwarders.interfaces';
7
7
 
@@ -42,7 +42,8 @@ function buildUserAttributesCopy(
42
42
 
43
43
  for (const prop in userAttributes) {
44
44
  if (
45
- !userAttributes.hasOwnProperty(prop) ||
45
+ !hasOwnProp(userAttributes, prop) ||
46
+ isReservedStoredPropertyName(prop) ||
46
47
  !isAttributeKeyAllowed(kitBlocker, prop)
47
48
  ) {
48
49
  continue;
@@ -64,7 +65,8 @@ function buildUserAttributeLists(
64
65
 
65
66
  for (const key in userAttributes) {
66
67
  if (
67
- !userAttributes.hasOwnProperty(key) ||
68
+ !hasOwnProp(userAttributes, key) ||
69
+ isReservedStoredPropertyName(key) ||
68
70
  !Array.isArray(userAttributes[key]) ||
69
71
  !isAttributeKeyAllowed(kitBlocker, key)
70
72
  ) {
@@ -79,19 +81,26 @@ function buildUserAttributeLists(
79
81
 
80
82
  function buildFilteredUserIdentities(
81
83
  identities: UserIdentities,
82
- kitBlocker: KitBlocker | undefined,
83
- parseNumber: (value: string | number) => number
84
+ kitBlocker: KitBlocker | undefined
84
85
  ): Dictionary<string> {
85
86
  const currentUserIdentities: Dictionary<string> = {};
86
87
  const identitiesByType = identities as Dictionary<string>;
87
88
 
88
89
  for (const identityType in identitiesByType) {
89
- if (!identitiesByType.hasOwnProperty(identityType)) {
90
+ if (!hasOwnProp(identitiesByType, identityType)) {
91
+ continue;
92
+ }
93
+
94
+ const storedIdentityType = getIdentityTypeFromStoredKey(identityType);
95
+
96
+ // Must be `=== null`: IdentityType.Other is 0, so a falsy check would
97
+ // drop a valid stored Other identity.
98
+ if (storedIdentityType === null) {
90
99
  continue;
91
100
  }
92
101
 
93
102
  const identityName = Types.IdentityType.getIdentityName(
94
- parseNumber(identityType)
103
+ storedIdentityType
95
104
  );
96
105
 
97
106
  if (!isIdentityAllowed(kitBlocker, identityName)) {
@@ -126,8 +135,7 @@ export default function filteredMparticleUser(
126
135
  getUserIdentities: function(): { userIdentities: Dictionary<string> } {
127
136
  let currentUserIdentities = buildFilteredUserIdentities(
128
137
  mpInstance._Store.getUserIdentities(mpid),
129
- kitBlocker,
130
- mpInstance._Helpers.parseNumber
138
+ kitBlocker
131
139
  );
132
140
 
133
141
  currentUserIdentities = mpInstance._Helpers.filterUserIdentitiesForForwarders(
package/src/forwarders.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import filteredMparticleUser, {
2
2
  isIdentityAllowed,
3
3
  } from './filteredMparticleUser';
4
- import { isEmpty, extend, Dictionary } from './utils';
4
+ import { isEmpty, extend, hasOwnProp, Dictionary } from './utils';
5
5
  import KitFilterHelper from './kitFilterHelper';
6
6
  import Constants from './constants';
7
7
  import APIClient, { IForwardingStatsData } from './apiClient';
@@ -69,7 +69,7 @@ function userAttributesMatchFilter(
69
69
  filterObject: Partial<IFilteringUserAttributeValue>
70
70
  ): boolean {
71
71
  for (const attrName in userAttributes) {
72
- if (!userAttributes.hasOwnProperty(attrName)) {
72
+ if (!hasOwnProp(userAttributes, attrName)) {
73
73
  continue;
74
74
  }
75
75
 
@@ -1,5 +1,5 @@
1
1
  import Constants, { ONE_DAY_IN_SECONDS, MILLIS_IN_ONE_SEC } from './constants';
2
- import { Dictionary, Environment, parseNumber, isObject, generateHash, generateUniqueId, isEmpty, isFunction, getErrorMessage } from './utils';
2
+ import { Dictionary, Environment, parseNumber, isObject, generateHash, generateUniqueId, hasOwnProp, isEmpty, isFunction, getErrorMessage } from './utils';
3
3
  import { BaseVault } from './vault';
4
4
  import Types from './types';
5
5
  import {
@@ -170,8 +170,7 @@ export const hasValidCachedIdentity = (
170
170
  );
171
171
  const hashedKey = generateHash(cacheKey);
172
172
 
173
- // if cache doesn't have the cacheKey, there is no valid cached identity
174
- if (!cache.hasOwnProperty(hashedKey)) {
173
+ if (!hasOwnProp(cache, hashedKey)) {
175
174
  return false;
176
175
  }
177
176
 
package/src/identity.ts CHANGED
@@ -15,10 +15,13 @@ const { CacheIdentity } = FeatureFlags;
15
15
  const { Identify, Modify, Login, Logout } = IdentityMethods;
16
16
  import {
17
17
  generateDeprecationMessage,
18
+ hasOwnProp,
18
19
  isEmpty,
20
+ isReservedStoredPropertyName,
19
21
  isFunction,
20
22
  isObject,
21
23
  } from './utils';
24
+ import { getIdentityTypeFromStoredKey } from './types';
22
25
  import { hasMPIDAndUserLoginChanged, hasMPIDChanged } from './user-utils';
23
26
  import { processReadyQueue } from './pre-init-utils';
24
27
  import { logDeprecatedMethodUsage } from './reporting/deprecatedMethodLogger';
@@ -893,13 +896,23 @@ export default function Identity(
893
896
  const identities = mpInstance._Store.getUserIdentities(mpid);
894
897
 
895
898
  for (const identityType in identities) {
896
- if (identities.hasOwnProperty(identityType)) {
897
- currentUserIdentities[
898
- Types.IdentityType.getIdentityName(
899
- mpInstance._Helpers.parseNumber(identityType)
900
- )
901
- ] = identities[identityType];
899
+ if (!hasOwnProp(identities, identityType)) {
900
+ continue;
901
+ }
902
+
903
+ const storedIdentityType = getIdentityTypeFromStoredKey(
904
+ identityType
905
+ );
906
+
907
+ // Must be `=== null`: IdentityType.Other is 0, so a falsy
908
+ // check would drop a valid stored Other identity.
909
+ if (storedIdentityType === null) {
910
+ continue;
902
911
  }
912
+
913
+ currentUserIdentities[
914
+ Types.IdentityType.getIdentityName(storedIdentityType)
915
+ ] = identities[identityType];
903
916
  }
904
917
 
905
918
  return {
@@ -1082,7 +1095,7 @@ export default function Identity(
1082
1095
 
1083
1096
  delete userAttributes[key];
1084
1097
 
1085
- if (cookies && cookies[mpid]) {
1098
+ if (cookies && isObject(cookies[mpid])) {
1086
1099
  cookies[mpid].ua = userAttributes;
1087
1100
  mpInstance._Persistence.savePersistence(cookies);
1088
1101
  }
@@ -1228,7 +1241,7 @@ export default function Identity(
1228
1241
  );
1229
1242
  if (userAttributes) {
1230
1243
  for (const prop in userAttributes) {
1231
- if (userAttributes.hasOwnProperty(prop)) {
1244
+ if (hasOwnProp(userAttributes, prop)) {
1232
1245
  mpInstance._Forwarders.handleForwarderUserAttributes(
1233
1246
  'removeUserAttribute',
1234
1247
  prop,
@@ -1252,7 +1265,8 @@ export default function Identity(
1252
1265
  userAttributes = this.getAllUserAttributes();
1253
1266
  for (const key in userAttributes) {
1254
1267
  if (
1255
- userAttributes.hasOwnProperty(key) &&
1268
+ hasOwnProp(userAttributes, key) &&
1269
+ !isReservedStoredPropertyName(key) &&
1256
1270
  Array.isArray(userAttributes[key])
1257
1271
  ) {
1258
1272
  userAttributesLists[key] = userAttributes[key].slice();
@@ -1273,7 +1287,10 @@ export default function Identity(
1273
1287
 
1274
1288
  if (userAttributes) {
1275
1289
  for (const prop in userAttributes) {
1276
- if (userAttributes.hasOwnProperty(prop)) {
1290
+ if (
1291
+ hasOwnProp(userAttributes, prop) &&
1292
+ !isReservedStoredPropertyName(prop)
1293
+ ) {
1277
1294
  const attrValue = userAttributes[prop];
1278
1295
  if (Array.isArray(attrValue)) {
1279
1296
  userAttributesCopy[prop] = attrValue.slice();
@@ -120,6 +120,17 @@ const integrationMappingExternal: IntegrationIdMapping = {
120
120
  mappedKey: 'SnapchatConversions.ClickId',
121
121
  output: IntegrationOutputs.CUSTOM_FLAGS,
122
122
  },
123
+ // Pinterest
124
+ // https://developers.pinterest.com/docs/track-conversions/track-conversions-in-the-api/
125
+ // https://help.pinterest.com/en/business/article/pinterest-tag-parameters-and-cookies
126
+ epik: {
127
+ mappedKey: 'Pinterest.click_id',
128
+ output: IntegrationOutputs.CUSTOM_FLAGS,
129
+ },
130
+ _epik: {
131
+ mappedKey: 'Pinterest.click_id',
132
+ output: IntegrationOutputs.CUSTOM_FLAGS,
133
+ },
123
134
  // Snapchat
124
135
  // https://developers.snap.com/api/marketing-api/Conversions-API/UsingTheAPI#sending-click-id
125
136
  _scid: {
@@ -175,6 +186,8 @@ export default class IntegrationCapture {
175
186
  const cookies = this.captureCookies() || {};
176
187
  const localStorage = this.captureLocalStorage() || {};
177
188
 
189
+ this.applyPinterestRules(queryParams, localStorage, cookies);
190
+
178
191
  // Facebook Rules
179
192
  // Exclude _fbc if fbclid is present
180
193
  // https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc#retrieve-from-fbclid-url-query-parameter
@@ -324,6 +337,52 @@ export default class IntegrationCapture {
324
337
  return mappedClickIds;
325
338
  }
326
339
 
340
+ private normalizePinterestClickId(clickIds: Dictionary<string>): void {
341
+ // Deterministic tie-breaker when both aliases are present in the same source:
342
+ // keep _epik and drop epik.
343
+ if (!isEmpty(clickIds?.['_epik']) && !isEmpty(clickIds?.['epik'])) {
344
+ delete clickIds['epik'];
345
+ }
346
+ }
347
+
348
+ private hasPinterestAlias(clickIds: Dictionary<string>): boolean {
349
+ return !isEmpty(clickIds?.['epik']) || !isEmpty(clickIds?.['_epik']);
350
+ }
351
+
352
+ private applyPinterestRules(
353
+ queryParams: Dictionary<string>,
354
+ localStorage: Dictionary<string>,
355
+ cookies: Dictionary<string>,
356
+ ): void {
357
+ this.normalizePinterestClickId(queryParams);
358
+ this.normalizePinterestClickId(localStorage);
359
+ this.normalizePinterestClickId(cookies);
360
+
361
+ // Cross-source precedence: query params > localStorage > cookies.
362
+ // Within the same source, prefer _epik when both aliases are present.
363
+ if (
364
+ this.hasPinterestAlias(queryParams) ||
365
+ this.hasPinterestAlias(localStorage) ||
366
+ this.hasPinterestAlias(cookies)
367
+ ) {
368
+ delete this.clickIds?.['epik'];
369
+ delete this.clickIds?.['_epik'];
370
+ }
371
+
372
+ if (this.hasPinterestAlias(queryParams)) {
373
+ delete cookies['epik'];
374
+ delete cookies['_epik'];
375
+ delete localStorage['epik'];
376
+ delete localStorage['_epik'];
377
+ return;
378
+ }
379
+
380
+ if (this.hasPinterestAlias(localStorage)) {
381
+ delete cookies['epik'];
382
+ delete cookies['_epik'];
383
+ }
384
+ }
385
+
327
386
  private applyProcessors(
328
387
  clickIds: Dictionary<string>,
329
388
  url?: string,
@@ -1454,7 +1454,9 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1454
1454
  mpInstance.Logger.verbose(
1455
1455
  'mParticle APV: [sdk-init] creating new PageViewTracker for this instance'
1456
1456
  );
1457
- mpInstance._PageViewTracker = new PageViewTracker(mpInstance);
1457
+ mpInstance._PageViewTracker = new PageViewTracker(mpInstance, {
1458
+ isAutoPageView: true,
1459
+ });
1458
1460
  }
1459
1461
 
1460
1462
  // The initial page view fires once per hard page load, not once per
@@ -1467,7 +1469,7 @@ function completeSDKInitialization(apiKey, config, mpInstance) {
1467
1469
  );
1468
1470
  } else {
1469
1471
  markInitialPageViewFired();
1470
- mpInstance._Events.logPageView();
1472
+ mpInstance._Events.logPageView({ isAutoPageView: true });
1471
1473
  }
1472
1474
 
1473
1475
  mpInstance._PageViewTracker.init();
@@ -127,6 +127,13 @@ interface IPageSnapshot {
127
127
  interface IPageViewData extends IPageSnapshot {
128
128
  hostname: string;
129
129
  title: string;
130
+ isAutoPageView?: boolean;
131
+ }
132
+
133
+ // Options for the emitters the AutoLogPageView flag drives. Passing this is how
134
+ // an emitter is told it is automatic; it never reads the feature flag itself.
135
+ export interface IPageViewOptions {
136
+ isAutoPageView?: boolean;
130
137
  }
131
138
 
132
139
  interface IPendingNavigation {
@@ -147,6 +154,16 @@ interface IPendingNavigation {
147
154
  export const allowedQueryParams = (href: string): Dictionary<string> =>
148
155
  queryStringParser(href, ALLOWED_QUERY_PARAMS);
149
156
 
157
+ export const AUTO_PAGE_VIEW_ATTRIBUTE = 'is_auto_page_view';
158
+
159
+ // Absent rather than false on a manual page view, so the attribute is only ever
160
+ // added by the automatic emitters. Shared by both of them so the key and that
161
+ // rule have one definition instead of two that can drift.
162
+ export const autoPageViewAttribute = (
163
+ isAutoPageView?: boolean
164
+ ): Dictionary<boolean> =>
165
+ isAutoPageView ? { [AUTO_PAGE_VIEW_ATTRIBUTE]: true } : {};
166
+
150
167
  // The captured params, in allowlist order. Ordering comes from the constant
151
168
  // rather than a sort: it is deterministic without needing a comparator, and it
152
169
  // does not depend on the object's insertion order, so reordering the query string
@@ -200,6 +217,7 @@ export const buildPageViewEvent = ({
200
217
  hostname,
201
218
  title,
202
219
  path,
220
+ isAutoPageView,
203
221
  }: IPageViewData): BaseEvent => ({
204
222
  messageType: MessageType.PageView,
205
223
  name: 'PageView',
@@ -211,6 +229,7 @@ export const buildPageViewEvent = ({
211
229
  hostname,
212
230
  title,
213
231
  path,
232
+ ...autoPageViewAttribute(isAutoPageView),
214
233
  },
215
234
  eventType: EventType.Unknown,
216
235
  });
@@ -411,8 +430,14 @@ export class PageViewTracker {
411
430
  private undoHistoryPatch: (() => void) | null = null;
412
431
  private popStateListener: (() => void) | null = null;
413
432
 
414
- constructor(mpInstance: IMParticleWebSDKInstance) {
433
+ private readonly isAutoPageView: boolean;
434
+
435
+ constructor(
436
+ mpInstance: IMParticleWebSDKInstance,
437
+ options?: IPageViewOptions
438
+ ) {
415
439
  this.mpInstance = mpInstance;
440
+ this.isAutoPageView = !!(options && options.isAutoPageView);
416
441
  }
417
442
 
418
443
  // True while this tracker owns the history patch and is listening for
@@ -589,6 +614,7 @@ export class PageViewTracker {
589
614
  ...page,
590
615
  hostname: window.location.hostname,
591
616
  title,
617
+ isAutoPageView: this.isAutoPageView,
592
618
  });
593
619
 
594
620
  this.log(
@@ -1023,7 +1023,7 @@ export default function _Persistence(
1023
1023
  if (csd) {
1024
1024
  let persistence = self.getPersistence();
1025
1025
  if (persistence) {
1026
- if (persistence[mpid]) {
1026
+ if (mpInstance._Helpers.isObject(persistence[mpid])) {
1027
1027
  persistence[mpid].csd = csd;
1028
1028
  } else {
1029
1029
  persistence[mpid] = {
@@ -1140,7 +1140,7 @@ export default function _Persistence(
1140
1140
  }
1141
1141
  let persistence = self.getPersistence();
1142
1142
  if (persistence) {
1143
- if (!persistence[mpid]) {
1143
+ if (!mpInstance._Helpers.isObject(persistence[mpid])) {
1144
1144
  persistence[mpid] = {};
1145
1145
  }
1146
1146
  if (!persistence[mpid].fst) {
@@ -1180,7 +1180,7 @@ export default function _Persistence(
1180
1180
  time = new Date().getTime();
1181
1181
  }
1182
1182
  let persistence = self.getPersistence();
1183
- if (persistence && persistence[mpid]) {
1183
+ if (persistence && mpInstance._Helpers.isObject(persistence[mpid])) {
1184
1184
  persistence[mpid].lst = time;
1185
1185
  self.savePersistence(persistence);
1186
1186
  }
package/src/store.ts CHANGED
@@ -533,7 +533,7 @@ export default function Store(
533
533
  this.syncPersistenceData();
534
534
 
535
535
  if (this.persistenceData) {
536
- if (this.persistenceData[mpid]) {
536
+ if (isObject(this.persistenceData[mpid])) {
537
537
  this.persistenceData[mpid][key] = value;
538
538
  } else {
539
539
  this.persistenceData[mpid] = {