-
Notifications
You must be signed in to change notification settings - Fork 58
Expand file tree
/
Copy pathintegrationCapture.ts
More file actions
414 lines (368 loc) · 14.7 KB
/
Copy pathintegrationCapture.ts
File metadata and controls
414 lines (368 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
import { SDKEventCustomFlags } from './sdkRuntimeModels';
import Constants from './constants';
import { IntegrationAttributes } from './store';
import {
Dictionary,
queryStringParser,
getCookies,
getHref,
isEmpty,
valueof,
} from './utils';
export interface IntegrationCaptureProcessorFunction {
(clickId: string, url: string, timestamp?: number): string;
}
// Facebook Click ID has specific formatting rules
// The formatted ClickID value must be of the form version.subdomainIndex.creationTime.<fbclid>, where:
// - version is always this prefix: fb
// - subdomainIndex is which domain the cookie is defined on ('com' = 0, 'example.com' = 1, 'www.example.com' = 2)
// - creationTime is the UNIX time since epoch in milliseconds when the _fbc was stored. If you don't save the _fbc cookie, use the timestamp when you first observed or received this fbclid value
// - <fbclid> is the value for the fbclid query parameter in the page URL.
// https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc
export const facebookClickIdProcessor: IntegrationCaptureProcessorFunction = (
clickId: string,
url: string,
timestamp?: number,
): string => {
if (!clickId || !url) {
return '';
}
const urlSegments = url?.split('//')
if (!urlSegments) {
return '';
}
const urlParts = urlSegments[1].split('/');
const domainParts = urlParts[0].split('.');
let subdomainIndex: number = 1;
// The rules for subdomainIndex are for parsing the domain portion
// of the URL for cookies, but in this case we are parsing the URL
// itself, so we can ignore the use of 0 for 'com'
if (domainParts.length >= 3) {
subdomainIndex = 2;
}
// If timestamp is not provided, use the current time
const _timestamp = timestamp || Date.now();
return `fb.${subdomainIndex}.${_timestamp}.${clickId}`;
};
// Integration outputs are used to determine how click ids are used within the SDK
// CUSTOM_FLAGS are sent out when an Event is created via ServerModel.createEventObject
// PARTNER_IDENTITIES are sent out in a Batch when a group of events are converted to a Batch
// INTEGRATION_ATTRIBUTES are stored initially on the SDKEvent level but then is added to the Batch when the batch is created
const IntegrationOutputs = {
CUSTOM_FLAGS: 'custom_flags',
PARTNER_IDENTITIES: 'partner_identities',
INTEGRATION_ATTRIBUTES: 'integration_attributes',
} as const;
interface IntegrationMappingItem {
mappedKey: string;
output: valueof<typeof IntegrationOutputs>;
processor?: IntegrationCaptureProcessorFunction;
moduleId?: number;
}
interface IntegrationIdMapping {
[key: string]: IntegrationMappingItem;
}
const integrationMappingExternal: IntegrationIdMapping = {
// Facebook / Meta
fbclid: {
mappedKey: 'Facebook.ClickId',
processor: facebookClickIdProcessor,
output: IntegrationOutputs.CUSTOM_FLAGS,
},
_fbp: {
mappedKey: 'Facebook.BrowserId',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
_fbc: {
mappedKey: 'Facebook.ClickId',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
// Google
gclid: {
mappedKey: 'GoogleEnhancedConversions.Gclid',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
gbraid: {
mappedKey: 'GoogleEnhancedConversions.Gbraid',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
wbraid: {
mappedKey: 'GoogleEnhancedConversions.Wbraid',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
// TIKTOK
ttclid: {
mappedKey: 'TikTok.Callback',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
_ttp: {
mappedKey: 'tiktok_cookie_id',
output: IntegrationOutputs.PARTNER_IDENTITIES,
},
// Snapchat
// https://businesshelp.snapchat.com/s/article/troubleshooting-click-id?language=en_US
ScCid: {
mappedKey: 'SnapchatConversions.ClickId',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
// Pinterest
// https://developers.pinterest.com/docs/track-conversions/track-conversions-in-the-api/
// https://help.pinterest.com/en/business/article/pinterest-tag-parameters-and-cookies
epik: {
mappedKey: 'Pinterest.click_id',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
_epik: {
mappedKey: 'Pinterest.click_id',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
// Snapchat
// https://developers.snap.com/api/marketing-api/Conversions-API/UsingTheAPI#sending-click-id
_scid: {
mappedKey: 'SnapchatConversions.Cookie1',
output: IntegrationOutputs.CUSTOM_FLAGS,
},
};
const integrationMappingRokt: IntegrationIdMapping = {
// Rokt
// https://docs.rokt.com/developers/integration-guides/web/advanced/rokt-id-tag/
// https://go.mparticle.com/work/SQDSDKS-7167
rtid: {
mappedKey: 'passbackconversiontrackingid',
output: IntegrationOutputs.INTEGRATION_ATTRIBUTES,
moduleId: 1277,
},
rclid: {
mappedKey: 'passbackconversiontrackingid',
output: IntegrationOutputs.INTEGRATION_ATTRIBUTES,
moduleId: 1277,
},
RoktTransactionId: {
mappedKey: 'passbackconversiontrackingid',
output: IntegrationOutputs.INTEGRATION_ATTRIBUTES,
moduleId: 1277,
},
};
export default class IntegrationCapture {
public clickIds: Dictionary<string>;
public readonly initialTimestamp: number;
public readonly filteredPartnerIdentityMappings: IntegrationIdMapping;
public readonly filteredCustomFlagMappings: IntegrationIdMapping;
public readonly filteredIntegrationAttributeMappings: IntegrationIdMapping;
public captureMode?: valueof<typeof Constants.CaptureIntegrationSpecificIdsV2Modes>;
constructor(captureMode: valueof<typeof Constants.CaptureIntegrationSpecificIdsV2Modes>) {
this.initialTimestamp = Date.now();
this.captureMode = captureMode;
// Cache filtered mappings for faster access
this.filteredPartnerIdentityMappings = this.filterMappings(IntegrationOutputs.PARTNER_IDENTITIES);
this.filteredCustomFlagMappings = this.filterMappings(IntegrationOutputs.CUSTOM_FLAGS);
this.filteredIntegrationAttributeMappings = this.filterMappings(IntegrationOutputs.INTEGRATION_ATTRIBUTES);
}
/**
* Captures Integration Ids from cookies and query params and stores them in clickIds object
*/
public capture(): void {
const queryParams = this.captureQueryParams() || {};
const cookies = this.captureCookies() || {};
const localStorage = this.captureLocalStorage() || {};
// Facebook Rules
// Exclude _fbc if fbclid is present
// https://developers.facebook.com/docs/marketing-api/conversions-api/parameters/fbp-and-fbc#retrieve-from-fbclid-url-query-parameter
if (queryParams['fbclid'] && cookies['_fbc']) {
delete cookies['_fbc'];
}
// Pinterest Rules
// epik and _epik both map to Pinterest.click_id. Prefer query params over
// localStorage and cookies (same rationale as Facebook fbclid vs _fbc).
// https://help.pinterest.com/en/business/article/pinterest-tag-parameters-and-cookies
const hasPinterestQuery =
!isEmpty(queryParams['epik']) || !isEmpty(queryParams['_epik']);
if (hasPinterestQuery) {
delete cookies['epik'];
delete cookies['_epik'];
delete localStorage['epik'];
delete localStorage['_epik'];
} else {
const hasPinterestLocalStorage =
!isEmpty(localStorage['epik']) || !isEmpty(localStorage['_epik']);
if (hasPinterestLocalStorage) {
delete cookies['epik'];
delete cookies['_epik'];
}
}
// ROKT Rules
// If both rtid or rclid and RoktTransactionId are present, prioritize rtid/rclid
// If RoktTransactionId is present in both cookies and localStorage,
// prioritize localStorage
const hasQueryParamId = queryParams['rtid'] || queryParams['rclid'];
const hasLocalStorageId = localStorage['RoktTransactionId'];
const hasCookieId = cookies['RoktTransactionId'];
if (hasQueryParamId) {
// Query param takes precedence, remove both localStorage and cookie if present
if (hasLocalStorageId) {
delete localStorage['RoktTransactionId'];
}
if (hasCookieId) {
delete cookies['RoktTransactionId'];
}
} else if (hasLocalStorageId && hasCookieId) {
// No query param, but both localStorage and cookie exist
// localStorage takes precedence over cookie
delete cookies['RoktTransactionId'];
}
this.clickIds = {
...this.clickIds,
...queryParams,
...localStorage,
...cookies
};
}
/**
* Captures cookies based on the integration ID mapping.
*/
public captureCookies(): Dictionary<string> {
const integrationKeys = this.getAllowedKeysForMode();
const cookies = getCookies(integrationKeys);
return this.applyProcessors(cookies, getHref(), this.initialTimestamp);
}
/**
* Captures query parameters based on the integration ID mapping.
*/
public captureQueryParams(): Dictionary<string> {
const queryParams = this.getQueryParams();
return this.applyProcessors(queryParams, getHref(), this.initialTimestamp);
}
/**
* Captures local storage based on the integration ID mapping.
*/
public captureLocalStorage(): Dictionary<string> {
const integrationKeys = this.getAllowedKeysForMode();
let localStorageItems: Dictionary<string> = {};
for (const key of integrationKeys) {
const localStorageItem = localStorage.getItem(key);
if (localStorageItem) {
localStorageItems[key] = localStorageItem;
}
}
return this.applyProcessors(localStorageItems, getHref(), this.initialTimestamp);
}
/**
* Gets the query parameters based on the integration ID mapping.
* @returns {Dictionary<string>} The query parameters.
*/
public getQueryParams(): Dictionary<string> {
const integrationKeys = this.getAllowedKeysForMode();
return queryStringParser(getHref(), integrationKeys);
}
/**
* Converts captured click IDs to custom flags for SDK events.
* @returns {SDKEventCustomFlags} The custom flags.
*/
public getClickIdsAsCustomFlags(): SDKEventCustomFlags {
return this.getClickIds(this.clickIds, this.filteredCustomFlagMappings);
}
/**
* Returns only the `partner_identities` mapped integration output.
* @returns {Dictionary<string>} The partner identities.
*/
public getClickIdsAsPartnerIdentities(): Dictionary<string> {
return this.getClickIds(this.clickIds, this.filteredPartnerIdentityMappings);
}
/**
* Returns only the `integration_attributes` mapped integration output.
* @returns {IntegrationAttributes} The integration attributes.
*/
public getClickIdsAsIntegrationAttributes(): IntegrationAttributes {
// Integration IDs are stored in the following format:
// {
// "integration_attributes": {
// "<moduleId>": {
// "mappedKey": "clickIdValue"
// }
// }
// }
const mappedClickIds: IntegrationAttributes = {};
for (const key in this.clickIds) {
if (this.clickIds.hasOwnProperty(key)) {
const value = this.clickIds[key];
const mappingKey = this.filteredIntegrationAttributeMappings[key]?.mappedKey;
if (!isEmpty(mappingKey)) {
const moduleId = this.filteredIntegrationAttributeMappings[key]?.moduleId;
if (moduleId && !mappedClickIds[moduleId]) {
mappedClickIds[moduleId] = { [mappingKey]: value };
}
}
}
}
return mappedClickIds;
}
private getClickIds(
clickIds: Dictionary<string>,
mappingList: IntegrationIdMapping
): Dictionary<string> {
const mappedClickIds: Dictionary<string> = {};
if (!clickIds) {
return mappedClickIds;
}
for (const key in clickIds) {
if (clickIds.hasOwnProperty(key)) {
const value = clickIds[key];
const mappedKey = mappingList[key]?.mappedKey;
if (!isEmpty(mappedKey)) {
mappedClickIds[mappedKey] = value;
}
}
}
return mappedClickIds;
}
private applyProcessors(
clickIds: Dictionary<string>,
url?: string,
timestamp?: number
): Dictionary<string> {
const processedClickIds: Dictionary<string> = {};
const integrationKeys = this.getActiveIntegrationMapping();
for (const key in clickIds) {
if (clickIds.hasOwnProperty(key)) {
const value = clickIds[key];
const processor = integrationKeys[key]?.processor;
processedClickIds[key] = processor ? processor(value, url, timestamp) : value;
}
}
return processedClickIds;
}
private filterMappings(
outputType: valueof<typeof IntegrationOutputs>
): IntegrationIdMapping {
const filteredMappings: IntegrationIdMapping = {};
const integrationKeys = this.getActiveIntegrationMapping();
for (const key in integrationKeys) {
if (integrationKeys[key].output === outputType) {
filteredMappings[key] = integrationKeys[key];
}
}
return filteredMappings;
}
/**
* Returns the allowed keys to capture based on the current mode.
* For RoktOnly, limit capture to Rokt keys; for All, capture all mapped keys.
*/
private getAllowedKeysForMode(): string[] {
return Object.keys(this.getActiveIntegrationMapping());
}
/**
* Selects the active integration mapping for the current captureMode.
* - 'roktonly': only Rokt IDs are considered
* - 'all': both External and Rokt IDs are considered
* - else: returns an empty mapping and nothing will be captured
*/
private getActiveIntegrationMapping(): IntegrationIdMapping {
if (this.captureMode === Constants.CaptureIntegrationSpecificIdsV2Modes.RoktOnly) {
return integrationMappingRokt;
}
if (this.captureMode === Constants.CaptureIntegrationSpecificIdsV2Modes.All) {
return { ...integrationMappingExternal, ...integrationMappingRokt };
}
return {};
}
}