@mparticle/web-sdk 3.23.0 → 3.25.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 +1 -1688
- package/dist/mparticle.esm.js +1 -1688
- package/dist/mparticle.js +1015 -253
- package/dist/types/src/buttonTracking/buttonClickEvent.d.ts +7 -0
- package/dist/types/src/buttonTracking/buttonDescription.d.ts +10 -3
- package/dist/types/src/buttonTracking/buttonTracker.d.ts +8 -0
- package/dist/types/src/buttonTracking/checkoutIntent.d.ts +8 -3
- package/dist/types/src/buttonTracking/controls.d.ts +8 -4
- package/dist/types/src/buttonTracking/labelMasking.d.ts +1 -0
- package/dist/types/src/configAPIClient.d.ts +1 -0
- package/dist/types/src/constants.d.ts +1 -0
- package/dist/types/src/cookieConsentManager.d.ts +17 -3
- package/dist/types/src/forwarder-utils.d.ts +3 -0
- package/dist/types/src/pageViewTracker.d.ts +19 -3
- package/dist/types/src/roktLauncherOptions.d.ts +1 -0
- package/dist/types/src/roktManager.d.ts +13 -2
- package/dist/types/src/sdkRuntimeModels.d.ts +1 -0
- package/dist/types/src/store.d.ts +3 -0
- package/package.json +21 -14
- package/src/buttonTracking/buttonClickEvent.ts +126 -8
- package/src/buttonTracking/buttonDescription.ts +336 -53
- package/src/buttonTracking/buttonTracker.ts +231 -19
- package/src/buttonTracking/checkoutIntent.ts +117 -23
- package/src/buttonTracking/controls.ts +169 -55
- package/src/buttonTracking/labelMasking.ts +4 -1
- package/src/configAPIClient.ts +1 -0
- package/src/constants.ts +1 -0
- package/src/cookieConsentManager.ts +23 -3
- package/src/cookieSyncManager.ts +11 -3
- package/src/events.ts +9 -3
- package/src/forwarder-utils.ts +38 -0
- package/src/forwarders.ts +2 -28
- package/src/mp-instance.ts +41 -7
- package/src/pageUrl.ts +43 -2
- package/src/pageViewTracker.ts +193 -111
- package/src/roktLauncherOptions.ts +1 -0
- package/src/roktManager.ts +66 -11
- package/src/sdkRuntimeModels.ts +1 -0
- package/src/store.ts +17 -0
package/src/pageUrl.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { getHref, valueof } from './utils';
|
|
|
3
3
|
|
|
4
4
|
// The one rule for a URL that leaves the SDK, shared with kits through
|
|
5
5
|
// `mParticle._cleanUrl`. `path` keeps scheme, host and path. `full` adds the
|
|
6
|
-
// query params an auto page view
|
|
6
|
+
// query params an auto page view captures by default and the campaign
|
|
7
|
+
// attribution params, then the fragment.
|
|
7
8
|
export const UrlMode = {
|
|
8
9
|
Path: 'path',
|
|
9
10
|
Full: 'full',
|
|
@@ -11,6 +12,46 @@ export const UrlMode = {
|
|
|
11
12
|
|
|
12
13
|
export type UrlMode = valueof<typeof UrlMode>;
|
|
13
14
|
|
|
15
|
+
// Campaign attribution and ad-network click ids. URLs keep them; an auto page view
|
|
16
|
+
// carries one as an attribute only when an input configures it.
|
|
17
|
+
const URL_ATTRIBUTION_QUERY_PARAMS: string[] = [
|
|
18
|
+
'utm_source',
|
|
19
|
+
'utm_medium',
|
|
20
|
+
'utm_campaign',
|
|
21
|
+
'utm_term',
|
|
22
|
+
'utm_content',
|
|
23
|
+
'utm_id',
|
|
24
|
+
'utm_source_platform',
|
|
25
|
+
'utm_creative_format',
|
|
26
|
+
'utm_marketing_tactic',
|
|
27
|
+
'gad_source',
|
|
28
|
+
'gad_campaignid',
|
|
29
|
+
'mc_cid',
|
|
30
|
+
'gclid',
|
|
31
|
+
'gbraid',
|
|
32
|
+
'wbraid',
|
|
33
|
+
'fbclid',
|
|
34
|
+
'msclkid',
|
|
35
|
+
'ttclid',
|
|
36
|
+
'twclid',
|
|
37
|
+
'li_fat_id',
|
|
38
|
+
'dclid',
|
|
39
|
+
'srsltid',
|
|
40
|
+
'epik',
|
|
41
|
+
'ScCid',
|
|
42
|
+
'irclickid',
|
|
43
|
+
'rtid',
|
|
44
|
+
'rclid',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const URL_ATTRIBUTION_QUERY_PARAMS_LOWER: string[] = URL_ATTRIBUTION_QUERY_PARAMS.map(
|
|
48
|
+
name => name.toLowerCase()
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const isUrlQueryParam = (name: string): boolean =>
|
|
52
|
+
isAllowedQueryParam(name) ||
|
|
53
|
+
URL_ATTRIBUTION_QUERY_PARAMS_LOWER.indexOf(name.toLowerCase()) !== -1;
|
|
54
|
+
|
|
14
55
|
// OAuth 2.0 / OIDC authorization-flow params. They stay off URLs even where the
|
|
15
56
|
// page view allowlist admits them, because a URL travels on every event.
|
|
16
57
|
export const EXCLUDED_URL_QUERY_PARAMS: string[] = [
|
|
@@ -55,7 +96,7 @@ const cleanQueryPair = (pair: string): string | null => {
|
|
|
55
96
|
const lowerName = name.toLowerCase();
|
|
56
97
|
if (
|
|
57
98
|
!name ||
|
|
58
|
-
!
|
|
99
|
+
!isUrlQueryParam(name) ||
|
|
59
100
|
EXCLUDED_URL_QUERY_PARAMS.indexOf(lowerName) !== -1
|
|
60
101
|
) {
|
|
61
102
|
return null;
|
package/src/pageViewTracker.ts
CHANGED
|
@@ -37,57 +37,9 @@ type WindowWithApv = Window & {
|
|
|
37
37
|
[WIN_APV_KEY]?: IApvState;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// none of those should reach the event stream because someone forgot to exclude
|
|
43
|
-
// them. Anything absent from this list is dropped.
|
|
44
|
-
//
|
|
45
|
-
// SECURITY: `code`, `state` and `nonce` are the OAuth 2.0 / OIDC authorization
|
|
46
|
-
// code and the CSRF/replay tokens. They are credentials until redeemed, and
|
|
47
|
-
// attaching them here persists them in the event store and forwards them to
|
|
48
|
-
// every configured kit. They are on the list by explicit product decision —
|
|
49
|
-
// deleting that line is the whole of the fix if that decision is revisited.
|
|
40
|
+
// Parameters that identify which page a view is of. Authorization-flow and campaign
|
|
41
|
+
// attribution parameters are deliberately absent; an input can add them explicitly.
|
|
50
42
|
export const ALLOWED_QUERY_PARAMS: string[] = [
|
|
51
|
-
// Campaign attribution
|
|
52
|
-
'utm_source',
|
|
53
|
-
'utm_medium',
|
|
54
|
-
'utm_campaign',
|
|
55
|
-
'utm_term',
|
|
56
|
-
'utm_content',
|
|
57
|
-
'utm_id',
|
|
58
|
-
'utm_source_platform',
|
|
59
|
-
'utm_creative_format',
|
|
60
|
-
'utm_marketing_tactic',
|
|
61
|
-
'gad_source',
|
|
62
|
-
'gad_campaignid',
|
|
63
|
-
'mc_cid',
|
|
64
|
-
|
|
65
|
-
// Ad-network click ids
|
|
66
|
-
'gclid',
|
|
67
|
-
'gbraid',
|
|
68
|
-
'wbraid',
|
|
69
|
-
'fbclid',
|
|
70
|
-
'msclkid',
|
|
71
|
-
'ttclid',
|
|
72
|
-
'twclid',
|
|
73
|
-
'li_fat_id',
|
|
74
|
-
'dclid',
|
|
75
|
-
'srsltid',
|
|
76
|
-
'epik',
|
|
77
|
-
'ScCid',
|
|
78
|
-
'irclickid',
|
|
79
|
-
'rtid',
|
|
80
|
-
'rclid',
|
|
81
|
-
|
|
82
|
-
// OAuth / OIDC — see the SECURITY note above
|
|
83
|
-
'client_id',
|
|
84
|
-
'redirect_uri',
|
|
85
|
-
'response_type',
|
|
86
|
-
'scope',
|
|
87
|
-
'state',
|
|
88
|
-
'code',
|
|
89
|
-
'nonce',
|
|
90
|
-
|
|
91
43
|
// Pagination
|
|
92
44
|
'page',
|
|
93
45
|
'limit',
|
|
@@ -132,6 +84,47 @@ export const ALLOWED_QUERY_PARAMS: string[] = [
|
|
|
132
84
|
'referrer',
|
|
133
85
|
];
|
|
134
86
|
|
|
87
|
+
export const AUTO_PAGE_VIEW_ATTRIBUTE = 'is_auto_page_view';
|
|
88
|
+
|
|
89
|
+
const CORE_EVENT_FIELDS = ['hostname', 'title', 'path', AUTO_PAGE_VIEW_ATTRIBUTE];
|
|
90
|
+
|
|
91
|
+
const PROTOTYPE_MEMBER_NAMES = ['constructor', '__proto__', 'prototype'];
|
|
92
|
+
|
|
93
|
+
// Server flag values are the strings 'True' and 'False', so these get typed here by habit.
|
|
94
|
+
const FLAG_VALUE_NAMES = ['true', 'false'];
|
|
95
|
+
|
|
96
|
+
const RESERVED_QUERY_PARAMS = CORE_EVENT_FIELDS.concat(
|
|
97
|
+
PROTOTYPE_MEMBER_NAMES,
|
|
98
|
+
FLAG_VALUE_NAMES
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const BUILT_IN_NAMES = ALLOWED_QUERY_PARAMS.map(name => name.toLowerCase());
|
|
102
|
+
|
|
103
|
+
const isBuiltIn = (name: string): boolean =>
|
|
104
|
+
BUILT_IN_NAMES.indexOf(name.toLowerCase()) !== -1;
|
|
105
|
+
|
|
106
|
+
// Excludes `&`, `=`, `%` and whitespace, which would distort the dedup key.
|
|
107
|
+
const QUERY_PARAM_NAME = /^[a-z0-9_][a-z0-9_.-]{0,63}$/;
|
|
108
|
+
|
|
109
|
+
export const MAX_CUSTOM_QUERY_PARAMS = 25;
|
|
110
|
+
|
|
111
|
+
export interface IQueryParamAllowlist {
|
|
112
|
+
allowed: string[];
|
|
113
|
+
|
|
114
|
+
// Positions, never names: a rejected entry may be a secret pasted into the wrong field.
|
|
115
|
+
rejectedPositions: number[];
|
|
116
|
+
|
|
117
|
+
overLimit: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const MAX_CUSTOM_QUERY_PARAM_VALUE_LENGTH = 512;
|
|
121
|
+
|
|
122
|
+
// A list, not a dictionary: its order is the order the dedup key is built in.
|
|
123
|
+
export interface ICapturedParam {
|
|
124
|
+
name: string;
|
|
125
|
+
value: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
135
128
|
// Params matched by name prefix, for platforms that put a per-product id in the
|
|
136
129
|
// name. Salesforce Commerce Cloud spells a variant choice
|
|
137
130
|
// `dwvar_<productId>_<attribute>`, e.g. `dwvar_12345_color=navy`.
|
|
@@ -140,22 +133,13 @@ export const ALLOWED_QUERY_PARAM_PREFIXES: string[] = ['dwvar_'];
|
|
|
140
133
|
const hasAllowedPrefix = (name: string): boolean =>
|
|
141
134
|
ALLOWED_QUERY_PARAM_PREFIXES.some(prefix => name.indexOf(prefix) === 0);
|
|
142
135
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// Whether a query param name, in any case, is one an auto page view captures.
|
|
148
|
-
export const isAllowedQueryParam = (name: string): boolean => {
|
|
149
|
-
const lower = name.toLowerCase();
|
|
150
|
-
return (
|
|
151
|
-
ALLOWED_QUERY_PARAMS_LOWER.indexOf(lower) !== -1 ||
|
|
152
|
-
hasAllowedPrefix(lower)
|
|
153
|
-
);
|
|
154
|
-
};
|
|
136
|
+
// Whether a query param name, in any case, is one an auto page view captures by default.
|
|
137
|
+
export const isAllowedQueryParam = (name: string): boolean =>
|
|
138
|
+
isBuiltIn(name) || hasAllowedPrefix(name.toLowerCase());
|
|
155
139
|
|
|
156
140
|
interface IPageSnapshot {
|
|
157
141
|
path: string;
|
|
158
|
-
params:
|
|
142
|
+
params: ICapturedParam[];
|
|
159
143
|
}
|
|
160
144
|
|
|
161
145
|
interface IPageViewData extends IPageSnapshot {
|
|
@@ -180,25 +164,114 @@ interface IPendingNavigation {
|
|
|
180
164
|
// on their own, which is where the interesting rules live.
|
|
181
165
|
// ---------------------------------------------------------------------------
|
|
182
166
|
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
167
|
+
// Validated here as well as in the dashboard, because remote config is untrusted input.
|
|
168
|
+
export const parseQueryParamAllowlist = (
|
|
169
|
+
configured: unknown
|
|
170
|
+
): IQueryParamAllowlist => {
|
|
171
|
+
const allowed: string[] = [];
|
|
172
|
+
const rejectedPositions: number[] = [];
|
|
173
|
+
let overLimit = 0;
|
|
174
|
+
|
|
175
|
+
if (!configured) {
|
|
176
|
+
return { allowed, rejectedPositions, overLimit };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const entries: unknown[] = Array.isArray(configured)
|
|
180
|
+
? configured
|
|
181
|
+
: String(configured).split(',');
|
|
182
|
+
|
|
183
|
+
entries.forEach((entry, index) => {
|
|
184
|
+
const position = index + 1;
|
|
185
|
+
const name = String(entry)
|
|
186
|
+
.trim()
|
|
187
|
+
.toLowerCase();
|
|
188
|
+
|
|
189
|
+
if (!name) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
!QUERY_PARAM_NAME.test(name) ||
|
|
195
|
+
RESERVED_QUERY_PARAMS.indexOf(name) !== -1
|
|
196
|
+
) {
|
|
197
|
+
rejectedPositions.push(position);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (isBuiltIn(name) || allowed.indexOf(name) !== -1) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (allowed.length >= MAX_CUSTOM_QUERY_PARAMS) {
|
|
206
|
+
overLimit++;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
allowed.push(name);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return { allowed, rejectedPositions, overLimit };
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// Not localeCompare: this order feeds the dedup key, so it must be identical in every
|
|
217
|
+
// browser, and localeCompare is locale-dependent.
|
|
218
|
+
const byName = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
|
|
219
|
+
|
|
220
|
+
// Built-ins keep their positions and additions sort after them, so a dedup key depends on
|
|
221
|
+
// which params are configured, not the order they were typed.
|
|
222
|
+
export const effectiveAllowlist = (extras: string[] = []): string[] =>
|
|
223
|
+
ALLOWED_QUERY_PARAMS.concat(
|
|
224
|
+
extras.filter(name => !isBuiltIn(name)).sort(byName)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Over-long custom values are dropped rather than truncated: two truncated values could
|
|
228
|
+
// share a dedup key and swallow a real page view.
|
|
229
|
+
const isWithinValueLimit = (name: string, value: string): boolean =>
|
|
230
|
+
isBuiltIn(name) || value.length <= MAX_CUSTOM_QUERY_PARAM_VALUE_LENGTH;
|
|
231
|
+
|
|
232
|
+
// A prefix match is a platform name, not a configured extra, so the custom
|
|
233
|
+
// value limit does not apply. A name already on the allowlist keeps that
|
|
234
|
+
// position instead of being appended again.
|
|
235
|
+
const prefixMatchedParams = (
|
|
236
|
+
href: string,
|
|
237
|
+
allowlist: string[]
|
|
238
|
+
): ICapturedParam[] => {
|
|
190
239
|
const allParams = queryStringParser(href);
|
|
191
240
|
|
|
192
|
-
Object.keys(allParams)
|
|
193
|
-
.filter(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
241
|
+
return Object.keys(allParams)
|
|
242
|
+
.filter(
|
|
243
|
+
name =>
|
|
244
|
+
hasAllowedPrefix(name) &&
|
|
245
|
+
allParams[name] &&
|
|
246
|
+
allowlist.indexOf(name) === -1
|
|
247
|
+
)
|
|
248
|
+
.sort(byName)
|
|
249
|
+
.map(name => ({ name, value: allParams[name] }));
|
|
250
|
+
};
|
|
197
251
|
|
|
198
|
-
|
|
252
|
+
export const allowedQueryParams = (
|
|
253
|
+
href: string,
|
|
254
|
+
extras: string[] = []
|
|
255
|
+
): ICapturedParam[] => {
|
|
256
|
+
const allowlist = effectiveAllowlist(extras);
|
|
257
|
+
const found = queryStringParser(href, allowlist);
|
|
258
|
+
const listed = allowlist
|
|
259
|
+
.filter(name => hasOwnProp(found, name))
|
|
260
|
+
.filter(name => isWithinValueLimit(name, found[name]))
|
|
261
|
+
.map(name => ({ name, value: found[name] }));
|
|
262
|
+
|
|
263
|
+
return listed.concat(prefixMatchedParams(href, allowlist));
|
|
199
264
|
};
|
|
200
265
|
|
|
201
|
-
export const
|
|
266
|
+
export const paramsToAttributes = (
|
|
267
|
+
params: ICapturedParam[]
|
|
268
|
+
): Dictionary<string> => {
|
|
269
|
+
const attributes: Dictionary<string> = {};
|
|
270
|
+
params.forEach(({ name, value }) => {
|
|
271
|
+
attributes[name] = value;
|
|
272
|
+
});
|
|
273
|
+
return attributes;
|
|
274
|
+
};
|
|
202
275
|
|
|
203
276
|
// Absent rather than false on a manual page view, so the attribute is only ever
|
|
204
277
|
// added by the automatic emitters. Shared by both of them so the key and that
|
|
@@ -208,35 +281,11 @@ export const autoPageViewAttribute = (
|
|
|
208
281
|
): Dictionary<boolean> =>
|
|
209
282
|
isAutoPageView ? { [AUTO_PAGE_VIEW_ATTRIBUTE]: true } : {};
|
|
210
283
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
// string cannot produce a different key.
|
|
214
|
-
//
|
|
215
|
-
// Membership is an own-property check, not `in`. `in` walks the prototype
|
|
216
|
-
// chain, so a name matching an Object.prototype member reports as present on
|
|
217
|
-
// any plain object. ALLOWED_QUERY_PARAMS contains no such name, which was the
|
|
218
|
-
// only thing making `in` safe here — and it stops being a safe assumption the
|
|
219
|
-
// moment this list can be extended from configuration.
|
|
220
|
-
const capturedNames = (params: Dictionary<string>): string[] =>
|
|
221
|
-
ALLOWED_QUERY_PARAMS.filter(name => hasOwnProp(params, name)).concat(
|
|
222
|
-
Object.keys(params)
|
|
223
|
-
.filter(hasAllowedPrefix)
|
|
224
|
-
.sort()
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
// The dedup key: pathname plus the allowlisted params in a fixed order, so that
|
|
228
|
-
// reordering the query string is not a new page. Params outside the allowlist
|
|
229
|
-
// never make it into `page.params` and so cannot key a view — nor can the hash.
|
|
230
|
-
//
|
|
231
|
-
// Values are re-encoded because queryStringParser hands them back DECODED. A
|
|
232
|
-
// value holding the pair delimiters would otherwise serialize exactly like two
|
|
233
|
-
// separate params — `{q: 'a&search=b'}` and `{q: 'a', search: 'b'}` both becoming
|
|
234
|
-
// `q=a&search=b` — and dedup would treat a real navigation between them as the
|
|
235
|
-
// same page and drop the view. `q`, `search` and `redirect_uri` carry `&` and `=`
|
|
236
|
-
// routinely, so this is reachable rather than theoretical.
|
|
284
|
+
// Values are re-encoded because they arrive decoded: `{q: 'a&search=b'}` would otherwise
|
|
285
|
+
// key the same as `{q: 'a', search: 'b'}` and dedup a real navigation away.
|
|
237
286
|
export const pageKey = (page: IPageSnapshot): string => {
|
|
238
|
-
const query =
|
|
239
|
-
.map(name => `${name}=${encodeURIComponent(
|
|
287
|
+
const query = page.params
|
|
288
|
+
.map(({ name, value }) => `${name}=${encodeURIComponent(value)}`)
|
|
240
289
|
.join('&');
|
|
241
290
|
|
|
242
291
|
return query ? `${page.path}?${query}` : page.path;
|
|
@@ -266,7 +315,7 @@ export const buildPageViewEvent = ({
|
|
|
266
315
|
// wins. No allowlist entry collides with hostname/title/path today; naming
|
|
267
316
|
// them here is what keeps a later addition from silently overwriting one.
|
|
268
317
|
data: {
|
|
269
|
-
...params,
|
|
318
|
+
...paramsToAttributes(params),
|
|
270
319
|
hostname,
|
|
271
320
|
title,
|
|
272
321
|
path,
|
|
@@ -356,9 +405,9 @@ const clearActiveTracker = (tracker: PageViewTracker): void => {
|
|
|
356
405
|
}
|
|
357
406
|
};
|
|
358
407
|
|
|
359
|
-
const currentPage = (): IPageSnapshot => ({
|
|
408
|
+
const currentPage = (extras: string[]): IPageSnapshot => ({
|
|
360
409
|
path: window.location.pathname,
|
|
361
|
-
params: allowedQueryParams(getHref()),
|
|
410
|
+
params: allowedQueryParams(getHref(), extras),
|
|
362
411
|
});
|
|
363
412
|
|
|
364
413
|
// Log-safe description of a page: the path, plus the NAMES of the captured
|
|
@@ -372,7 +421,7 @@ const describePage = (page: IPageSnapshot | null): string => {
|
|
|
372
421
|
return '';
|
|
373
422
|
}
|
|
374
423
|
|
|
375
|
-
const names =
|
|
424
|
+
const names = page.params.map(({ name }) => name);
|
|
376
425
|
return names.length ? `${page.path} (params: ${names.join()})` : page.path;
|
|
377
426
|
};
|
|
378
427
|
|
|
@@ -390,6 +439,8 @@ export class PageViewTracker {
|
|
|
390
439
|
|
|
391
440
|
private lastPage: IPageSnapshot | null = null;
|
|
392
441
|
private active = false;
|
|
442
|
+
|
|
443
|
+
private customQueryParams: string[] = [];
|
|
393
444
|
private pendingNavigations: IPendingNavigation[] = [];
|
|
394
445
|
|
|
395
446
|
private undoHistoryPatch: (() => void) | null = null;
|
|
@@ -434,7 +485,8 @@ export class PageViewTracker {
|
|
|
434
485
|
}
|
|
435
486
|
|
|
436
487
|
this.active = true;
|
|
437
|
-
this.
|
|
488
|
+
this.customQueryParams = this.readCustomQueryParams();
|
|
489
|
+
this.lastPage = currentPage(this.customQueryParams);
|
|
438
490
|
this.log(`[init] seeded lastPage: ${describePage(this.lastPage)}`);
|
|
439
491
|
|
|
440
492
|
this.undoHistoryPatch = subscribeToRouteChange(
|
|
@@ -467,6 +519,36 @@ export class PageViewTracker {
|
|
|
467
519
|
clearActiveTracker(this);
|
|
468
520
|
}
|
|
469
521
|
|
|
522
|
+
private readCustomQueryParams(): string[] {
|
|
523
|
+
const {
|
|
524
|
+
allowed = [],
|
|
525
|
+
rejectedPositions = [],
|
|
526
|
+
overLimit = 0,
|
|
527
|
+
} = (this.mpInstance._Store.SDKConfig.autoLogPageViewQueryParams ||
|
|
528
|
+
{}) as IQueryParamAllowlist;
|
|
529
|
+
|
|
530
|
+
if (rejectedPositions.length) {
|
|
531
|
+
this.mpInstance.Logger.warning(
|
|
532
|
+
'mParticle APV: ignoring invalid additional page view query ' +
|
|
533
|
+
`parameters at positions ${rejectedPositions.join(', ')}`
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (overLimit) {
|
|
538
|
+
this.mpInstance.Logger.warning(
|
|
539
|
+
`mParticle APV: ignoring ${overLimit} additional page view ` +
|
|
540
|
+
`query parameters beyond the limit of ` +
|
|
541
|
+
`${MAX_CUSTOM_QUERY_PARAMS}`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (allowed.length) {
|
|
546
|
+
this.log(`[init] additional query params: ${allowed.join()}`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return allowed;
|
|
550
|
+
}
|
|
551
|
+
|
|
470
552
|
// Stops the outgoing tracker and returns the pages it had queued so this
|
|
471
553
|
// tracker can re-schedule them once it is active. Private, but reachable
|
|
472
554
|
// across instances: the handoff protocol stays internal to the class.
|
|
@@ -514,7 +596,7 @@ export class PageViewTracker {
|
|
|
514
596
|
}
|
|
515
597
|
|
|
516
598
|
private handleNavigation(source: NavigationSource): void {
|
|
517
|
-
const candidate = currentPage();
|
|
599
|
+
const candidate = currentPage(this.customQueryParams);
|
|
518
600
|
const lastKey = this.lastPage ? pageKey(this.lastPage) : null;
|
|
519
601
|
|
|
520
602
|
this.log(
|
package/src/roktManager.ts
CHANGED
|
@@ -23,10 +23,11 @@ import { PARTNER_MODULE_IDS } from "./cookieSyncManager";
|
|
|
23
23
|
import IntegrationCapture from "./integrationCapture";
|
|
24
24
|
import Constants from "./constants";
|
|
25
25
|
import { subscribeToRouteChange } from "./routeChangeMonitor";
|
|
26
|
-
import type { CheckoutIntentKind, CheckoutIntentSource } from "./buttonTracking/checkoutIntent";
|
|
26
|
+
import type { CheckoutIntentKind, CheckoutIntentSource, PaymentFrameFamily } from "./buttonTracking/checkoutIntent";
|
|
27
27
|
|
|
28
28
|
const PASSBACK_CONVERSION_TRACKING_ID = 'passbackconversiontrackingid';
|
|
29
|
-
|
|
29
|
+
// One signal per kind (commit, wallet, checkout_step, frame and cart), so a new kind never evicts another.
|
|
30
|
+
const CHECKOUT_INTENT_BUFFER_LIMIT = 5;
|
|
30
31
|
const CHECKOUT_INTENT_MAX_AGE_MS = 30000;
|
|
31
32
|
|
|
32
33
|
type AttributeTargetUser = Pick<IMParticleUser, 'getUserIdentities' | 'setUserAttributes'>;
|
|
@@ -84,6 +85,17 @@ export interface ICheckoutIntentSignal {
|
|
|
84
85
|
pathname?: string;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// What a kit receives when the shopper picks a payment method: the names of a
|
|
89
|
+
// payment radio's option, masked like a label, or the payment family of a
|
|
90
|
+
// frame the shopper moved into. It is handed over in memory only: the kit
|
|
91
|
+
// keeps the family it reads and nothing of the names.
|
|
92
|
+
export interface IPaymentPickSignal {
|
|
93
|
+
t: number;
|
|
94
|
+
pathname: string;
|
|
95
|
+
names?: readonly string[];
|
|
96
|
+
family?: PaymentFrameFamily;
|
|
97
|
+
}
|
|
98
|
+
|
|
87
99
|
export interface IRoktKitSettings {
|
|
88
100
|
accountId?: string;
|
|
89
101
|
placementAttributesMapping?: string;
|
|
@@ -108,6 +120,8 @@ export interface IRoktKit {
|
|
|
108
120
|
onRouteChange?: () => void;
|
|
109
121
|
// Set by the kit only when it wants checkout intent signals.
|
|
110
122
|
onCheckoutIntent?: (signal: ICheckoutIntentSignal) => void;
|
|
123
|
+
// Set by the kit only when it wants payment picks.
|
|
124
|
+
onPaymentPick?: (signal: IPaymentPickSignal) => void;
|
|
111
125
|
launcherOptions?: Dictionary<any>;
|
|
112
126
|
settings?: IRoktKitSettings;
|
|
113
127
|
integrationName?: string;
|
|
@@ -156,6 +170,8 @@ export default class RoktManager {
|
|
|
156
170
|
private stopRouteChangeWatch: (() => void) | null = null;
|
|
157
171
|
// Checkout intent raised before the kit attached: latest per kind.
|
|
158
172
|
private pendingCheckoutIntents: Array<{ signal: ICheckoutIntentSignal; pathname: string }> = [];
|
|
173
|
+
// Off until SDK init finds the Rokt connection receives auto button clicks.
|
|
174
|
+
private autoButtonClicksForwarded = false;
|
|
159
175
|
|
|
160
176
|
// Keys this manager's route subscription, so a re-executed bundle's manager replaces
|
|
161
177
|
// it while a second named instance keeps its own.
|
|
@@ -308,21 +324,38 @@ export default class RoktManager {
|
|
|
308
324
|
}
|
|
309
325
|
}
|
|
310
326
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
327
|
+
public setAutoButtonClicksForwarded(forwarded: boolean): void {
|
|
328
|
+
this.autoButtonClicksForwarded = forwarded;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// noTargeting (which noDeviceId implies), doNotShareOrSell or Global Privacy
|
|
332
|
+
// Control. Unlike noFunctional, these also stop auto button and link clicks
|
|
333
|
+
// from being logged.
|
|
334
|
+
public hasTargetingOptOut(): boolean {
|
|
318
335
|
const options = this.launcherOptions || {};
|
|
319
|
-
if (options.
|
|
336
|
+
if (options.noTargeting || options.doNotShareOrSell) {
|
|
320
337
|
return true;
|
|
321
338
|
}
|
|
322
339
|
const nav = typeof navigator === 'undefined' ? undefined : (navigator as Navigator & { globalPrivacyControl?: boolean });
|
|
323
340
|
return !!nav && nav.globalPrivacyControl === true;
|
|
324
341
|
}
|
|
325
342
|
|
|
343
|
+
// Checkout intent is off unless the flag is on, and under opt-out, any Rokt
|
|
344
|
+
// privacy launcher option, Global Privacy Control, or a Rokt connection that
|
|
345
|
+
// does not receive auto button clicks.
|
|
346
|
+
public isCheckoutIntentSuppressed(): boolean {
|
|
347
|
+
const flags = this.store?.SDKConfig?.flags;
|
|
348
|
+
if (
|
|
349
|
+
!flags ||
|
|
350
|
+
flags[Constants.FeatureFlags.AutoButtonTracking] !== true ||
|
|
351
|
+
!this.store.isEnabled ||
|
|
352
|
+
!this.autoButtonClicksForwarded
|
|
353
|
+
) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
return !!this.launcherOptions?.noFunctional || this.hasTargetingOptOut();
|
|
357
|
+
}
|
|
358
|
+
|
|
326
359
|
// True when the kit can wait for the shopper's confirm click: signals are not
|
|
327
360
|
// suppressed and the browser exposes the composed path the detector reads.
|
|
328
361
|
// Decided from config alone, so tracker setup and teardown never change it.
|
|
@@ -364,6 +397,26 @@ export default class RoktManager {
|
|
|
364
397
|
this.pendingCheckoutIntents = [];
|
|
365
398
|
}
|
|
366
399
|
|
|
400
|
+
// Under the same gates as checkout intent. A pick made before a kit
|
|
401
|
+
// attaches is not kept: the shopper is still choosing, and the next pick
|
|
402
|
+
// replaces it.
|
|
403
|
+
public notifyPaymentPick(signal: IPaymentPickSignal): void {
|
|
404
|
+
if (this.isCheckoutIntentSuppressed()) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const kit = this.kit;
|
|
409
|
+
try {
|
|
410
|
+
if (kit && isFunction(kit.onPaymentPick)) {
|
|
411
|
+
kit.onPaymentPick(signal);
|
|
412
|
+
}
|
|
413
|
+
} catch (e) {
|
|
414
|
+
this.logger?.error(
|
|
415
|
+
`RoktManager: Error in onPaymentPick: ${getErrorMessage(e)}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
367
420
|
// Replays recent buffered signals raised on the current page, oldest first.
|
|
368
421
|
private flushCheckoutIntents(): void {
|
|
369
422
|
const pending = this.pendingCheckoutIntents;
|
|
@@ -381,7 +434,9 @@ export default class RoktManager {
|
|
|
381
434
|
.forEach(({ signal }) => this.notifyCheckoutIntent(signal));
|
|
382
435
|
}
|
|
383
436
|
|
|
384
|
-
|
|
437
|
+
// The Rokt kit reads this to tell core's page views, whose title and utm
|
|
438
|
+
// attributes a filter may have removed, from ones logged by hand.
|
|
439
|
+
public isAutoLogPageViewEnabled(): boolean {
|
|
385
440
|
return (
|
|
386
441
|
this.store?.SDKConfig?.flags?.[
|
|
387
442
|
Constants.FeatureFlags.AutoLogPageView
|
package/src/sdkRuntimeModels.ts
CHANGED
package/src/store.ts
CHANGED
|
@@ -9,6 +9,10 @@ import {
|
|
|
9
9
|
} from '@mparticle/web-sdk';
|
|
10
10
|
import { IKitConfigs } from './configAPIClient';
|
|
11
11
|
import Constants from './constants';
|
|
12
|
+
import {
|
|
13
|
+
IQueryParamAllowlist,
|
|
14
|
+
parseQueryParamAllowlist,
|
|
15
|
+
} from './pageViewTracker';
|
|
12
16
|
import {
|
|
13
17
|
DataPlanResult,
|
|
14
18
|
KitBlockerOptions,
|
|
@@ -65,6 +69,7 @@ export interface SDKConfig {
|
|
|
65
69
|
appVersion?: string;
|
|
66
70
|
package?: string;
|
|
67
71
|
flags?: IFeatureFlags;
|
|
72
|
+
autoLogPageViewQueryParams?: IQueryParamAllowlist;
|
|
68
73
|
kitConfigs: IKitConfigs[];
|
|
69
74
|
kits: Dictionary<UnregisteredKit>;
|
|
70
75
|
logLevel?: LogLevelType;
|
|
@@ -154,6 +159,7 @@ export interface IFeatureFlags {
|
|
|
154
159
|
astBackgroundEvents?: boolean;
|
|
155
160
|
autoLogPageView?: boolean;
|
|
156
161
|
autoButtonTracking?: boolean;
|
|
162
|
+
autoLinkTracking?: boolean;
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
// Temporary Interface until Store can be refactored as a class
|
|
@@ -332,6 +338,9 @@ export default function Store(
|
|
|
332
338
|
}
|
|
333
339
|
|
|
334
340
|
this.SDKConfig.flags = processFlags(config);
|
|
341
|
+
this.SDKConfig.autoLogPageViewQueryParams = parseQueryParamAllowlist(
|
|
342
|
+
config.autoLogPageViewQueryParams
|
|
343
|
+
);
|
|
335
344
|
|
|
336
345
|
if (config.deviceId) {
|
|
337
346
|
this.deviceId = config.deviceId;
|
|
@@ -728,6 +737,12 @@ export default function Store(
|
|
|
728
737
|
this.SDKConfig.flags = processFlags(config);
|
|
729
738
|
}
|
|
730
739
|
|
|
740
|
+
// Stored whole: the tracker reports the rejections, and cannot recover them by
|
|
741
|
+
// re-parsing a list that has already had them removed.
|
|
742
|
+
this.SDKConfig.autoLogPageViewQueryParams = parseQueryParamAllowlist(
|
|
743
|
+
config.autoLogPageViewQueryParams
|
|
744
|
+
);
|
|
745
|
+
|
|
731
746
|
const baseUrls: Dictionary<string> = processBaseUrls(
|
|
732
747
|
config,
|
|
733
748
|
this.SDKConfig.flags,
|
|
@@ -779,6 +794,7 @@ export function processFlags(config: SDKInitConfig): IFeatureFlags {
|
|
|
779
794
|
AstBackgroundEvents,
|
|
780
795
|
AutoLogPageView,
|
|
781
796
|
AutoButtonTracking,
|
|
797
|
+
AutoLinkTracking,
|
|
782
798
|
} = Constants.FeatureFlags;
|
|
783
799
|
|
|
784
800
|
if (!config.flags) {
|
|
@@ -801,6 +817,7 @@ export function processFlags(config: SDKInitConfig): IFeatureFlags {
|
|
|
801
817
|
flags[AstBackgroundEvents] = config.flags[AstBackgroundEvents] === 'True';
|
|
802
818
|
flags[AutoLogPageView] = config.flags[AutoLogPageView] === 'True';
|
|
803
819
|
flags[AutoButtonTracking] = config.flags[AutoButtonTracking] === 'True';
|
|
820
|
+
flags[AutoLinkTracking] = config.flags[AutoLinkTracking] === 'True';
|
|
804
821
|
return flags;
|
|
805
822
|
}
|
|
806
823
|
|