@mparticle/web-sdk 3.6.2 → 3.8.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.
@@ -28,7 +28,7 @@ declare const replaceQuotesWithApostrophes: (value: string) => string;
28
28
  declare const replaceMPID: (value: string, mpid: MPID) => string;
29
29
  declare const replaceAmpWithAmpersand: (value: string) => string;
30
30
  declare const createCookieSyncUrl: (mpid: MPID, pixelUrl: string, redirectUrl?: string, domain?: string, base64Mpid?: string) => string;
31
- declare const toWebSafeBase64: (value: string) => string;
31
+ declare const toWebSafeBase64: (value: string) => string | undefined;
32
32
  declare const returnConvertedBoolean: (data: string | boolean | number) => boolean;
33
33
  declare const decoded: (s: string) => string;
34
34
  declare const converted: (s: string) => string;
@@ -43,7 +43,8 @@ declare const isStringOrNumber: (value: any) => boolean;
43
43
  declare const isEmpty: (value: Dictionary<any> | string | null | undefined) => boolean;
44
44
  declare const mergeObjects: <T extends object>(...objects: T[]) => T;
45
45
  declare const moveElementToEnd: <T>(array: T[], index: number) => T[];
46
- declare const hasOwnProp: (obj: object, key: string) => boolean;
46
+ declare const hasOwnProp: (obj: object, key: PropertyKey) => boolean;
47
+ declare const isReservedStoredPropertyName: (name: string) => boolean;
47
48
  declare const queryStringParser: (url: string, keys?: string[]) => Dictionary<string>;
48
49
  declare const getCookies: (keys?: string[]) => Dictionary<string>;
49
50
  declare const getHref: () => string;
@@ -83,4 +84,4 @@ declare const obfuscateData: (value: any) => any;
83
84
  declare const obfuscateDevData: (data: any, isDevelopmentMode?: boolean) => any;
84
85
  declare function extend(...args: any[]): any;
85
86
  declare const getErrorMessage: (e: unknown, fallback?: string) => string;
86
- export { createCookieString, revertCookieString, createCookieSyncUrl, toWebSafeBase64, valueof, AttributeValue, converted, decoded, obfuscateDevData, filterDictionaryWithHash, findKeyInObject, generateDeprecationMessage, generateHash, generateUniqueId, getErrorMessage, getRampNumber, inArray, isObject, isStringOrNumber, obfuscateData, parseConfig, parseNumber, parseSettingsString, parseStringOrNumber, replaceCommasWithPipes, replacePipesWithCommas, replaceApostrophesWithQuotes, replaceQuotesWithApostrophes, returnConvertedBoolean, toDataPlanSlug, isString, isNumber, isFunction, isDataPlanSlug, isEmpty, isValidAttributeValue, isValidCustomFlagProperty, mergeObjects, moveElementToEnd, queryStringParser, hasOwnProp, getCookies, getHref, replaceMPID, replaceAmpWithAmpersand, extend, };
87
+ export { createCookieString, revertCookieString, createCookieSyncUrl, toWebSafeBase64, valueof, AttributeValue, converted, decoded, obfuscateDevData, filterDictionaryWithHash, findKeyInObject, generateDeprecationMessage, generateHash, generateUniqueId, getErrorMessage, getRampNumber, inArray, isObject, isStringOrNumber, obfuscateData, parseConfig, parseNumber, parseSettingsString, parseStringOrNumber, replaceCommasWithPipes, replacePipesWithCommas, replaceApostrophesWithQuotes, replaceQuotesWithApostrophes, returnConvertedBoolean, toDataPlanSlug, isString, isNumber, isFunction, isDataPlanSlug, isEmpty, isValidAttributeValue, isValidCustomFlagProperty, mergeObjects, moveElementToEnd, queryStringParser, hasOwnProp, isReservedStoredPropertyName, getCookies, getHref, replaceMPID, replaceAmpWithAmpersand, extend, };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mparticle/web-sdk",
3
- "version": "3.6.2",
3
+ "version": "3.8.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
 
package/src/helpers.ts CHANGED
@@ -136,7 +136,15 @@ export default function Helpers(
136
136
  }
137
137
  return mostRecentUser || null;
138
138
  } else {
139
- return mpInstance.Identity.getUser(previousMpid);
139
+ const currentUser =
140
+ mParticleUser ||
141
+ mpInstance.Identity.getCurrentUser();
142
+ const userDidNotChange =
143
+ currentUser?.getMPID() === previousMpid;
144
+
145
+ return userDidNotChange
146
+ ? null
147
+ : mpInstance.Identity.getUser(previousMpid);
140
148
  }
141
149
  },
142
150
  } as unknown as IdentityResult);
@@ -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();
@@ -1548,7 +1565,8 @@ export default function Identity(
1548
1565
  callback,
1549
1566
  callbackCode,
1550
1567
  identityApiResult || null,
1551
- newUser
1568
+ newUser,
1569
+ previousMPID
1552
1570
  );
1553
1571
  } else if (
1554
1572
  identityApiResult &&
@@ -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();