forked from prebid/Prebid.js
-
Notifications
You must be signed in to change notification settings - Fork 1
/
id5IdSystem.js
532 lines (479 loc) · 17.7 KB
/
id5IdSystem.js
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
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
/**
* This module adds ID5 to the User ID module
* The {@link module:modules/userId} module is required
* @module modules/id5IdSystem
* @requires module:modules/userId
*/
import {
deepAccess,
deepSetValue,
isEmpty,
isEmptyStr,
isPlainObject,
logError,
logInfo,
logWarn
} from '../src/utils.js';
import {fetch} from '../src/ajax.js';
import {submodule} from '../src/hook.js';
import {getRefererInfo} from '../src/refererDetection.js';
import {getStorageManager} from '../src/storageManager.js';
import {gppDataHandler, uspDataHandler} from '../src/adapterManager.js';
import {MODULE_TYPE_UID} from '../src/activities/modules.js';
import {GreedyPromise} from '../src/utils/promise.js';
import {loadExternalScript} from '../src/adloader.js';
/**
* @typedef {import('../modules/userId/index.js').Submodule} Submodule
* @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig
* @typedef {import('../modules/userId/index.js').ConsentData} ConsentData
* @typedef {import('../modules/userId/index.js').IdResponse} IdResponse
*/
const MODULE_NAME = 'id5Id';
const GVLID = 131;
export const ID5_STORAGE_NAME = 'id5id';
const LOG_PREFIX = 'User ID - ID5 submodule: ';
const ID5_API_CONFIG_URL = 'https://id5-sync.com/api/config/prebid';
const ID5_DOMAIN = 'id5-sync.com';
const TRUE_LINK_SOURCE = 'true-link-id5-sync.com';
export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME});
/**
* @typedef {Object} IdResponse
* @property {string} [universal_uid] - The encrypted ID5 ID to pass to bidders
* @property {Object} [ext] - The extensions object to pass to bidders
* @property {Object} [ab_testing] - A/B testing configuration
*/
/**
* @typedef {Object} FetchCallConfig
* @property {string} [url] - The URL for the fetch endpoint
* @property {Object} [overrides] - Overrides to apply to fetch parameters
*/
/**
* @typedef {Object} ExtensionsCallConfig
* @property {string} [url] - The URL for the extensions endpoint
* @property {string} [method] - Overrides the HTTP method to use to make the call
* @property {Object} [body] - Specifies a body to pass to the extensions endpoint
*/
/**
* @typedef {Object} DynamicConfig
* @property {FetchCallConfig} [fetchCall] - The fetch call configuration
* @property {ExtensionsCallConfig} [extensionsCall] - The extensions call configuration
*/
/**
* @typedef {Object} ABTestingConfig
* @property {boolean} enabled - Tells whether A/B testing is enabled for this instance
* @property {number} controlGroupPct - A/B testing probability
*/
/**
* @typedef {Object} Multiplexing
* @property {boolean} [disabled] - Disable multiplexing (instance will work in single mode)
*/
/**
* @typedef {Object} Diagnostics
* @property {boolean} [publishingDisabled] - Disable diagnostics publishing
* @property {number} [publishAfterLoadInMsec] - Delay in ms after script load after which collected diagnostics are published
* @property {boolean} [publishBeforeWindowUnload] - When true, diagnostics publishing is triggered on Window 'beforeunload' event
* @property {number} [publishingSampleRatio] - Diagnostics publishing sample ratio
*/
/**
* @typedef {Object} Segment
* @property {string} [destination] - GVL ID or ID5-XX Partner ID. Mandatory
* @property {Array<string>} [ids] - The segment IDs to push. Must contain at least one segment ID.
*/
/**
* @typedef {Object} Id5PrebidConfig
* @property {number} partner - The ID5 partner ID
* @property {string} pd - The ID5 partner data string
* @property {ABTestingConfig} abTesting - The A/B testing configuration
* @property {boolean} disableExtensions - Disabled extensions call
* @property {string} [externalModuleUrl] - URL for the id5 prebid external module
* @property {Multiplexing} [multiplexing] - Multiplexing options. Only supported when loading the external module.
* @property {Diagnostics} [diagnostics] - Diagnostics options. Supported only in multiplexing
* @property {Array<Segment>} [segments] - A list of segments to push to partners. Supported only in multiplexing.
* @property {boolean} [disableUaHints] - When true, look up of high entropy values through user agent hints is disabled.
*/
/** @type {Submodule} */
export const id5IdSubmodule = {
/**
* used to link submodule with config
* @type {string}
*/
name: 'id5Id',
/**
* Vendor id of ID5
* @type {Number}
*/
gvlid: GVLID,
/**
* decode the stored id value for passing to bid requests
* @function decode
* @param {(Object|string)} value
* @param {SubmoduleConfig|undefined} config
* @returns {(Object|undefined)}
*/
decode(value, config) {
let universalUid, publisherTrueLinkId;
let ext = {};
if (value && typeof value.universal_uid === 'string') {
universalUid = value.universal_uid;
ext = value.ext || ext;
publisherTrueLinkId = value.publisherTrueLinkId;
} else {
return undefined;
}
let responseObj = {
id5id: {
uid: universalUid,
ext: ext
}
};
if (isPlainObject(ext.euid)) {
responseObj.euid = {
uid: ext.euid.uids[0].id,
source: ext.euid.source,
ext: {provider: ID5_DOMAIN}
};
}
if (publisherTrueLinkId) {
responseObj.trueLinkId = {
uid: publisherTrueLinkId,
};
}
const abTestingResult = deepAccess(value, 'ab_testing.result');
switch (abTestingResult) {
case 'control':
// A/B Testing is enabled and user is in the Control Group
logInfo(LOG_PREFIX + 'A/B Testing - user is in the Control Group: ID5 ID is NOT exposed');
deepSetValue(responseObj, 'id5id.ext.abTestingControlGroup', true);
break;
case 'error':
// A/B Testing is enabled, but configured improperly, so skip A/B testing
logError(LOG_PREFIX + 'A/B Testing ERROR! controlGroupPct must be a number >= 0 and <= 1');
break;
case 'normal':
// A/B Testing is enabled but user is not in the Control Group, so ID5 ID is shared
logInfo(LOG_PREFIX + 'A/B Testing - user is NOT in the Control Group');
deepSetValue(responseObj, 'id5id.ext.abTestingControlGroup', false);
break;
}
logInfo(LOG_PREFIX + 'Decoded ID', responseObj);
return responseObj;
},
/**
* performs action to obtain id and return a value in the callback's response argument
* @function getId
* @param {SubmoduleConfig} submoduleConfig
* @param {ConsentData} consentData
* @param {(Object|undefined)} cacheIdObj
* @returns {IdResponse|undefined}
*/
getId(submoduleConfig, consentData, cacheIdObj) {
if (!validateConfig(submoduleConfig)) {
return undefined;
}
if (!hasWriteConsentToLocalStorage(consentData)) {
logInfo(LOG_PREFIX + 'Skipping ID5 local storage write because no consent given.');
return undefined;
}
const resp = function (cbFunction) {
const fetchFlow = new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData(), gppDataHandler.getConsentData());
fetchFlow.execute()
.then(response => {
cbFunction(response);
})
.catch(error => {
logError(LOG_PREFIX + 'getId fetch encountered an error', error);
cbFunction();
});
};
return {callback: resp};
},
/**
* Similar to Submodule#getId, this optional method returns response to for id that exists already.
* If IdResponse#id is defined, then it will be written to the current active storage even if it exists already.
* If IdResponse#callback is defined, then it'll called at the end of auction.
* It's permissible to return neither, one, or both fields.
* @function extendId
* @param {SubmoduleConfig} config
* @param {ConsentData|undefined} consentData
* @param {Object} cacheIdObj - existing id, if any
* @return {IdResponse} A response object that contains id.
*/
extendId(config, consentData, cacheIdObj) {
if (!hasWriteConsentToLocalStorage(consentData)) {
logInfo(LOG_PREFIX + 'No consent given for ID5 local storage writing, skipping nb increment.');
return cacheIdObj;
}
logInfo(LOG_PREFIX + 'using cached ID', cacheIdObj);
if (cacheIdObj) {
cacheIdObj.nbPage = incrementNb(cacheIdObj)
}
return cacheIdObj;
},
primaryIds: ['id5id', 'trueLinkId'],
eids: {
'id5id': {
getValue: function (data) {
return data.uid;
},
source: ID5_DOMAIN,
atype: 1,
getUidExt: function (data) {
if (data.ext) {
return data.ext;
}
}
},
'euid': {
getValue: function (data) {
return data.uid;
},
getSource: function (data) {
return data.source;
},
atype: 3,
getUidExt: function (data) {
if (data.ext) {
return data.ext;
}
}
},
'trueLinkId': {
getValue: function (data) {
return data.uid;
},
getSource: function (data) {
return TRUE_LINK_SOURCE;
},
atype: 1,
getUidExt: function (data) {
if (data.ext) {
return data.ext;
}
}
}
}
};
export class IdFetchFlow {
constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData, gppData) {
this.submoduleConfig = submoduleConfig;
this.gdprConsentData = gdprConsentData;
this.cacheIdObj = cacheIdObj;
this.usPrivacyData = usPrivacyData;
this.gppData = gppData;
}
/**
* Calls the ID5 Servers to fetch an ID5 ID
* @returns {Promise<IdResponse>} The result of calling the server side
*/
async execute() {
const configCallPromise = this.#callForConfig();
if (this.#isExternalModule()) {
try {
return await this.#externalModuleFlow(configCallPromise);
} catch (error) {
logError(LOG_PREFIX + 'Error while performing ID5 external module flow. Continuing with regular flow.', error);
return this.#regularFlow(configCallPromise);
}
} else {
return this.#regularFlow(configCallPromise);
}
}
#isExternalModule() {
return typeof this.submoduleConfig.params.externalModuleUrl === 'string';
}
// eslint-disable-next-line no-dupe-class-members
async #externalModuleFlow(configCallPromise) {
await loadExternalModule(this.submoduleConfig.params.externalModuleUrl);
const fetchFlowConfig = await configCallPromise;
return this.#getExternalIntegration().fetchId5Id(fetchFlowConfig, this.submoduleConfig.params, getRefererInfo(), this.gdprConsentData, this.usPrivacyData, this.gppData);
}
// eslint-disable-next-line no-dupe-class-members
#getExternalIntegration() {
return window.id5Prebid && window.id5Prebid.integration;
}
// eslint-disable-next-line no-dupe-class-members
async #regularFlow(configCallPromise) {
const fetchFlowConfig = await configCallPromise;
const extensionsData = await this.#callForExtensions(fetchFlowConfig.extensionsCall);
const fetchCallResponse = await this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData);
return this.#processFetchCallResponse(fetchCallResponse);
}
// eslint-disable-next-line no-dupe-class-members
async #callForConfig() {
let url = this.submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify({
...this.submoduleConfig,
bounce: true
}),
credentials: 'include'
});
if (!response.ok) {
throw new Error('Error while calling config endpoint: ', response);
}
const dynamicConfig = await response.json();
logInfo(LOG_PREFIX + 'config response received from the server', dynamicConfig);
return dynamicConfig;
}
// eslint-disable-next-line no-dupe-class-members
async #callForExtensions(extensionsCallConfig) {
if (extensionsCallConfig === undefined) {
return undefined;
}
const extensionsUrl = extensionsCallConfig.url;
const method = extensionsCallConfig.method || 'GET';
const body = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {});
const response = await fetch(extensionsUrl, {method, body});
if (!response.ok) {
throw new Error('Error while calling extensions endpoint: ', response);
}
const extensions = await response.json();
logInfo(LOG_PREFIX + 'extensions response received from the server', extensions);
return extensions;
}
// eslint-disable-next-line no-dupe-class-members
async #callId5Fetch(fetchCallConfig, extensionsData) {
const fetchUrl = fetchCallConfig.url;
const additionalData = fetchCallConfig.overrides || {};
const body = JSON.stringify({
...this.#createFetchRequestData(),
...additionalData,
extensions: extensionsData
});
const response = await fetch(fetchUrl, {method: 'POST', body, credentials: 'include'});
if (!response.ok) {
throw new Error('Error while calling fetch endpoint: ', response);
}
const fetchResponse = await response.json();
logInfo(LOG_PREFIX + 'fetch response received from the server', fetchResponse);
return fetchResponse;
}
// eslint-disable-next-line no-dupe-class-members
#createFetchRequestData() {
const params = this.submoduleConfig.params;
const hasGdpr = (this.gdprConsentData && typeof this.gdprConsentData.gdprApplies === 'boolean' && this.gdprConsentData.gdprApplies) ? 1 : 0;
const referer = getRefererInfo();
const signature = this.cacheIdObj ? this.cacheIdObj.signature : undefined;
const nbPage = incrementNb(this.cacheIdObj);
const trueLinkInfo = window.id5Bootstrap ? window.id5Bootstrap.getTrueLinkInfo() : {booted: false};
const data = {
'partner': params.partner,
'gdpr': hasGdpr,
'nbPage': nbPage,
'o': 'pbjs',
'tml': referer.topmostLocation,
'ref': referer.ref,
'cu': referer.canonicalUrl,
'top': referer.reachedTop ? 1 : 0,
'u': referer.stack[0] || window.location.href,
'v': '$prebid.version$',
'storage': this.submoduleConfig.storage,
'localStorage': storage.localStorageIsEnabled() ? 1 : 0,
'true_link': trueLinkInfo
};
// pass in optional data, but only if populated
if (hasGdpr && this.gdprConsentData.consentString !== undefined && !isEmpty(this.gdprConsentData.consentString) && !isEmptyStr(this.gdprConsentData.consentString)) {
data.gdpr_consent = this.gdprConsentData.consentString;
}
if (this.usPrivacyData !== undefined && !isEmpty(this.usPrivacyData) && !isEmptyStr(this.usPrivacyData)) {
data.us_privacy = this.usPrivacyData;
}
if (this.gppData) {
data.gpp_string = this.gppData.gppString;
data.gpp_sid = this.gppData.applicableSections;
}
if (signature !== undefined && !isEmptyStr(signature)) {
data.s = signature;
}
if (params.pd !== undefined && !isEmptyStr(params.pd)) {
data.pd = params.pd;
}
if (params.provider !== undefined && !isEmptyStr(params.provider)) {
data.provider = params.provider;
}
const abTestingConfig = params.abTesting || {enabled: false};
if (abTestingConfig.enabled) {
data.ab_testing = {
enabled: true, control_group_pct: abTestingConfig.controlGroupPct // The server validates
};
}
return data;
}
// eslint-disable-next-line no-dupe-class-members
#processFetchCallResponse(fetchCallResponse) {
try {
if (fetchCallResponse.privacy) {
if (window.id5Bootstrap && window.id5Bootstrap.setPrivacy) {
window.id5Bootstrap.setPrivacy(fetchCallResponse.privacy);
}
}
} catch (error) {
logError(LOG_PREFIX + 'Error while writing privacy info into local storage.', error);
}
return fetchCallResponse;
}
}
async function loadExternalModule(url) {
return new GreedyPromise((resolve, reject) => {
if (window.id5Prebid) {
// Already loaded
resolve();
} else {
try {
loadExternalScript(url, 'id5', resolve);
} catch (error) {
reject(error);
}
}
});
}
function validateConfig(config) {
if (!config || !config.params || !config.params.partner) {
logError(LOG_PREFIX + 'partner required to be defined');
return false;
}
const partner = config.params.partner;
if (typeof partner === 'string' || partner instanceof String) {
let parsedPartnerId = parseInt(partner);
if (isNaN(parsedPartnerId) || parsedPartnerId < 0) {
logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer');
return false;
} else {
config.params.partner = parsedPartnerId;
}
} else if (typeof partner !== 'number') {
logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer');
return false;
}
if (!config.storage || !config.storage.type || !config.storage.name) {
logError(LOG_PREFIX + 'storage required to be set');
return false;
}
if (config.storage.name !== ID5_STORAGE_NAME) {
logWarn(LOG_PREFIX + `storage name recommended to be '${ID5_STORAGE_NAME}'.`);
}
return true;
}
function incrementNb(cachedObj) {
if (cachedObj && cachedObj.nbPage !== undefined) {
return cachedObj.nbPage + 1;
} else {
return 1;
}
}
/**
* Check to see if we can write to local storage based on purpose consent 1, and that we have vendor consent (ID5=131)
* @param {ConsentData} consentData
* @returns {boolean}
*/
function hasWriteConsentToLocalStorage(consentData) {
const hasGdpr = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies;
const localstorageConsent = deepAccess(consentData, `vendorData.purpose.consents.1`);
const id5VendorConsent = deepAccess(consentData, `vendorData.vendor.consents.${GVLID.toString()}`);
if (hasGdpr && (!localstorageConsent || !id5VendorConsent)) {
return false;
}
return true;
}
submodule('userId', id5IdSubmodule);