@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/dist/mparticle.common.js +61 -40
- package/dist/mparticle.esm.js +61 -40
- package/dist/mparticle.js +200 -88
- package/dist/types/src/events.interfaces.d.ts +2 -1
- package/dist/types/src/integrationCapture.d.ts +3 -0
- package/dist/types/src/pageViewTracker.d.ts +9 -2
- package/dist/types/src/types.d.ts +1 -0
- package/dist/types/src/utils.d.ts +4 -3
- package/package.json +1 -1
- package/src/consent.ts +3 -3
- package/src/cookieConsentManager.ts +2 -3
- package/src/cookieSyncManager.ts +82 -69
- package/src/events.interfaces.ts +2 -1
- package/src/events.ts +7 -2
- package/src/filteredMparticleUser.ts +18 -10
- package/src/forwarders.ts +2 -2
- package/src/identity-utils.ts +2 -3
- package/src/identity.ts +27 -10
- package/src/integrationCapture.ts +59 -0
- package/src/mp-instance.ts +4 -2
- package/src/pageViewTracker.ts +27 -1
- package/src/persistence.ts +3 -3
- package/src/store.ts +1 -1
- package/src/types.ts +14 -0
- package/src/utils.ts +29 -10
package/package.json
CHANGED
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
|
|
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
|
|
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
|
}
|
package/src/cookieSyncManager.ts
CHANGED
|
@@ -87,76 +87,89 @@ export default function CookieSyncManager(
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
pixelConfigurations.forEach((pixelSettings: IPixelConfiguration) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
package/src/events.interfaces.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
72
|
+
if (!hasOwnProp(userAttributes, attrName)) {
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
75
|
|
package/src/identity-utils.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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,
|
package/src/mp-instance.ts
CHANGED
|
@@ -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();
|
package/src/pageViewTracker.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
package/src/persistence.ts
CHANGED
|
@@ -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] = {
|