-
Notifications
You must be signed in to change notification settings - Fork 2.1k
/
index.js
1152 lines (1048 loc) · 42 KB
/
index.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
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* This module adds User ID support to prebid.js
* @module modules/userId
*/
/**
* @interface Submodule
*/
/**
* @function
* @summary performs action to obtain id and return a value in the callback's response argument.
* If IdResponse#id is defined, then it will be written to the current active storage.
* If IdResponse#callback is defined, then it'll called at the end of auction.
* It's permissible to return neither, one, or both fields.
* @name Submodule#getId
* @param {SubmoduleConfig} config
* @param {ConsentData|undefined} consentData
* @param {(Object|undefined)} cacheIdObj
* @return {(IdResponse|undefined)} A response object that contains id and/or callback.
*/
/**
* @function
* @summary 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.
* @name Submodule#extendId
* @param {SubmoduleConfig} config
* @param {ConsentData|undefined} consentData
* @param {Object} storedId - existing id, if any
* @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback.
*/
/**
* @function
* @summary decode a stored value for passing to bid requests
* @name Submodule#decode
* @param {Object|string} value
* @param {SubmoduleConfig|undefined} config
* @return {(Object|undefined)}
*/
/**
* @property
* @summary used to link submodule with config
* @name Submodule#name
* @type {string}
*/
/**
* @property
* @summary use a predefined domain override for cookies or provide your own
* @name Submodule#domainOverride
* @type {(undefined|function)}
*/
/**
* @function
* @summary Returns the root domain
* @name Submodule#findRootDomain
* @returns {string}
*/
/**
* @typedef {Object} SubmoduleConfig
* @property {string} name - the User ID submodule name (used to link submodule with config)
* @property {(SubmoduleStorage|undefined)} storage - browser storage config
* @property {(SubmoduleParams|undefined)} params - params config for use by the submodule.getId function
* @property {(Object|undefined)} value - if not empty, this value is added to bid requests for access in adapters
*/
/**
* @typedef {Object} SubmoduleStorage
* @property {string} type - browser storage type (html5 or cookie)
* @property {string} name - key name to use when saving/reading to local storage or cookies
* @property {number} expires - time to live for browser storage in days
* @property {(number|undefined)} refreshInSeconds - if not empty, this value defines the maximum time span in seconds before refreshing user ID stored in browser
*/
/**
* @typedef {Object} LiveIntentCollectConfig
* @property {(string|undefined)} fpiStorageStrategy - defines whether the first party identifiers that LiveConnect creates and updates are stored in a cookie jar, local storage, or not created at all
* @property {(number|undefined)} fpiExpirationDays - the expiration time of an identifier created and updated by LiveConnect
* @property {(string|undefined)} collectorUrl - defines where the LiveIntentId signal pixels are pointing to
* @property {(string|undefined)} appId - the unique identifier of the application in question
*/
/**
* @typedef {Object} SubmoduleParams
* @property {(string|undefined)} partner - partner url param value
* @property {(string|undefined)} url - webservice request url used to load Id data
* @property {(string|undefined)} pixelUrl - publisher pixel to extend/modify cookies
* @property {(boolean|undefined)} create - create id if missing. default is true.
* @property {(boolean|undefined)} extend - extend expiration time on each access. default is false.
* @property {(string|undefined)} pid - placement id url param value
* @property {(string|undefined)} publisherId - the unique identifier of the publisher in question
* @property {(string|undefined)} ajaxTimeout - the number of milliseconds a resolution request can take before automatically being terminated
* @property {(array|undefined)} identifiersToResolve - the identifiers from either ls|cookie to be attached to the getId query
* @property {(LiveIntentCollectConfig|undefined)} liCollectConfig - the config for LiveIntent's collect requests
* @property {(string|undefined)} pd - publisher provided data for reconciling ID5 IDs
* @property {(string|undefined)} emailHash - if provided, the hashed email address of a user
* @property {(string|undefined)} notUse3P - use to retrieve envelope from 3p endpoint
*/
/**
* @typedef {Object} SubmoduleContainer
* @property {Submodule} submodule
* @property {SubmoduleConfig} config
* @property {(Object|undefined)} idObj - cache decoded id value (this is copied to every adUnit bid)
* @property {(function|undefined)} callback - holds reference to submodule.getId() result if it returned a function. Will be set to undefined after callback executes
* @property {StorageManager} storageMgr
*/
/**
* @typedef {Object} ConsentData
* @property {(string|undefined)} consentString
* @property {(Object|undefined)} vendorData
* @property {(boolean|undefined)} gdprApplies
*/
/**
* @typedef {Object} IdResponse
* @property {(Object|undefined)} id - id data
* @property {(function|undefined)} callback - function that will return an id
*/
import {find, includes} from '../../src/polyfill.js';
import {config} from '../../src/config.js';
import * as events from '../../src/events.js';
import {getGlobal} from '../../src/prebidGlobal.js';
import adapterManager, {gdprDataHandler, gppDataHandler} from '../../src/adapterManager.js';
import CONSTANTS from '../../src/constants.json';
import {module, ready as hooksReady} from '../../src/hook.js';
import {buildEidPermissions, createEidsArray, USER_IDS_CONFIG} from './eids.js';
import {
getCoreStorageManager,
getStorageManager,
STORAGE_TYPE_COOKIES,
STORAGE_TYPE_LOCALSTORAGE
} from '../../src/storageManager.js';
import {
cyrb53Hash,
deepAccess,
deepSetValue,
delayExecution,
getPrebidInternal,
isArray,
isEmpty,
isEmptyStr,
isFn,
isGptPubadsDefined,
isNumber,
isPlainObject,
logError,
logInfo,
logWarn
} from '../../src/utils.js';
import {getPPID as coreGetPPID} from '../../src/adserver.js';
import {defer, GreedyPromise} from '../../src/utils/promise.js';
import {registerOrtbProcessor, REQUEST} from '../../src/pbjsORTB.js';
import {newMetrics, timedAuctionHook, useMetrics} from '../../src/utils/perfMetrics.js';
import {findRootDomain} from '../../src/fpd/rootDomain.js';
import {GDPR_GVLIDS} from '../../src/consentHandler.js';
import {MODULE_TYPE_UID} from '../../src/activities/modules.js';
import {isActivityAllowed} from '../../src/activities/rules.js';
import {ACTIVITY_ENRICH_EIDS} from '../../src/activities/activities.js';
import {activityParams} from '../../src/activities/activityParams.js';
const MODULE_NAME = 'User ID';
const COOKIE = STORAGE_TYPE_COOKIES;
const LOCAL_STORAGE = STORAGE_TYPE_LOCALSTORAGE;
const DEFAULT_SYNC_DELAY = 500;
const NO_AUCTION_DELAY = 0;
const CONSENT_DATA_COOKIE_STORAGE_CONFIG = {
name: '_pbjs_userid_consent_data',
expires: 30 // 30 days expiration, which should match how often consent is refreshed by CMPs
};
export const PBJS_USER_ID_OPTOUT_NAME = '_pbjs_id_optout';
export const coreStorage = getCoreStorageManager('userId');
export const dep = {
isAllowed: isActivityAllowed
}
/** @type {boolean} */
let addedUserIdHook = false;
/** @type {SubmoduleContainer[]} */
let submodules = [];
/** @type {SubmoduleContainer[]} */
let initializedSubmodules;
/** @type {SubmoduleConfig[]} */
let configRegistry = [];
/** @type {Object} */
let idPriority = {};
/** @type {Submodule[]} */
let submoduleRegistry = [];
/** @type {(number|undefined)} */
let timeoutID;
/** @type {(number|undefined)} */
export let syncDelay;
/** @type {(number|undefined)} */
export let auctionDelay;
/** @type {(string|undefined)} */
let ppidSource;
let configListener;
const uidMetrics = (() => {
let metrics;
return () => {
if (metrics == null) {
metrics = newMetrics();
}
return metrics;
}
})();
function submoduleMetrics(moduleName) {
return uidMetrics().fork().renameWith(n => [`userId.mod.${n}`, `userId.mods.${moduleName}.${n}`])
}
/** @param {Submodule[]} submodules */
export function setSubmoduleRegistry(submodules) {
submoduleRegistry = submodules;
}
function cookieSetter(submodule, storageMgr) {
storageMgr = storageMgr || submodule.storageMgr;
const domainOverride = (typeof submodule.submodule.domainOverride === 'function') ? submodule.submodule.domainOverride() : null;
const name = submodule.config.storage.name;
return function setCookie(suffix, value, expiration) {
storageMgr.setCookie(name + (suffix || ''), value, expiration, 'Lax', domainOverride);
}
}
/**
* @param {SubmoduleContainer} submodule
* @param {(Object|string)} value
*/
export function setStoredValue(submodule, value) {
/**
* @type {SubmoduleStorage}
*/
const storage = submodule.config.storage;
const mgr = submodule.storageMgr;
try {
const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString();
const valueStr = isPlainObject(value) ? JSON.stringify(value) : value;
if (storage.type === COOKIE) {
const setCookie = cookieSetter(submodule);
setCookie(null, valueStr, expiresStr);
if (typeof storage.refreshInSeconds === 'number') {
setCookie('_last', new Date().toUTCString(), expiresStr);
}
} else if (storage.type === LOCAL_STORAGE) {
mgr.setDataInLocalStorage(`${storage.name}_exp`, expiresStr);
mgr.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr));
if (typeof storage.refreshInSeconds === 'number') {
mgr.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString());
}
}
} catch (error) {
logError(error);
}
}
export function deleteStoredValue(submodule) {
let deleter, suffixes;
switch (submodule.config?.storage?.type) {
case COOKIE:
const setCookie = cookieSetter(submodule, coreStorage);
const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString();
deleter = (suffix) => setCookie(suffix, '', expiry)
suffixes = ['', '_last'];
break;
case LOCAL_STORAGE:
deleter = (suffix) => coreStorage.removeDataFromLocalStorage(submodule.config.storage.name + suffix)
suffixes = ['', '_last', '_exp'];
break;
}
if (deleter) {
suffixes.forEach(suffix => {
try {
deleter(suffix)
} catch (e) {
logError(e);
}
});
}
}
function setPrebidServerEidPermissions(initializedSubmodules) {
let setEidPermissions = getPrebidInternal().setEidPermissions;
if (typeof setEidPermissions === 'function' && isArray(initializedSubmodules)) {
setEidPermissions(buildEidPermissions(initializedSubmodules));
}
}
/**
* @param {SubmoduleContainer} submodule
* @param {String|undefined} key optional key of the value
* @returns {string}
*/
function getStoredValue(submodule, key = undefined) {
const mgr = submodule.storageMgr;
const storage = submodule.config.storage;
const storedKey = key ? `${storage.name}_${key}` : storage.name;
let storedValue;
try {
if (storage.type === COOKIE) {
storedValue = mgr.getCookie(storedKey);
} else if (storage.type === LOCAL_STORAGE) {
const storedValueExp = mgr.getDataFromLocalStorage(`${storage.name}_exp`);
// empty string means no expiration set
if (storedValueExp === '') {
storedValue = mgr.getDataFromLocalStorage(storedKey);
} else if (storedValueExp) {
if ((new Date(storedValueExp)).getTime() - Date.now() > 0) {
storedValue = decodeURIComponent(mgr.getDataFromLocalStorage(storedKey));
}
}
}
// support storing a string or a stringified object
if (typeof storedValue === 'string' && storedValue.trim().charAt(0) === '{') {
storedValue = JSON.parse(storedValue);
}
} catch (e) {
logError(e);
}
return storedValue;
}
/**
* makes an object that can be stored with only the keys we need to check.
* excluding the vendorConsents object since the consentString is enough to know
* if consent has changed without needing to have all the details in an object
* @param consentData
* @returns {{apiVersion: number, gdprApplies: boolean, consentString: string}}
*/
function makeStoredConsentDataHash(consentData) {
const storedConsentData = {
consentString: '',
gdprApplies: false,
apiVersion: 0
};
if (consentData) {
storedConsentData.consentString = consentData.consentString;
storedConsentData.gdprApplies = consentData.gdprApplies;
storedConsentData.apiVersion = consentData.apiVersion;
}
return cyrb53Hash(JSON.stringify(storedConsentData));
}
/**
* puts the current consent data into cookie storage
* @param consentData
*/
export function setStoredConsentData(consentData) {
try {
const expiresStr = (new Date(Date.now() + (CONSENT_DATA_COOKIE_STORAGE_CONFIG.expires * (60 * 60 * 24 * 1000)))).toUTCString();
coreStorage.setCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name, makeStoredConsentDataHash(consentData), expiresStr, 'Lax');
} catch (error) {
logError(error);
}
}
/**
* get the stored consent data from local storage, if any
* @returns {string}
*/
function getStoredConsentData() {
try {
return coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name);
} catch (e) {
logError(e);
}
}
/**
* test if the consent object stored locally matches the current consent data. if they
* don't match or there is nothing stored locally, it means a refresh of the user id
* submodule is needed
* @param storedConsentData
* @param consentData
* @returns {boolean}
*/
function storedConsentDataMatchesConsentData(storedConsentData, consentData) {
return (
typeof storedConsentData !== 'undefined' &&
storedConsentData !== null &&
storedConsentData === makeStoredConsentDataHash(consentData)
);
}
/**
* @param {SubmoduleContainer[]} submodules
* @param {function} cb - callback for after processing is done.
*/
function processSubmoduleCallbacks(submodules, cb, allModules) {
cb = uidMetrics().fork().startTiming('userId.callbacks.total').stopBefore(cb);
const done = delayExecution(() => {
clearTimeout(timeoutID);
cb();
}, submodules.length);
submodules.forEach(function (submodule) {
const moduleDone = submoduleMetrics(submodule.submodule.name).startTiming('callback').stopBefore(done);
function callbackCompleted(idObj) {
// if valid, id data should be saved to cookie/html storage
if (idObj) {
if (submodule.config.storage) {
setStoredValue(submodule, idObj);
}
// cache decoded value (this is copied to every adUnit bid)
submodule.idObj = submodule.submodule.decode(idObj, submodule.config);
updatePPID(getCombinedSubmoduleIds(allModules));
} else {
logInfo(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`);
}
moduleDone();
}
try {
submodule.callback(callbackCompleted, getStoredValue.bind(null, submodule));
} catch (e) {
logError(`Error in userID module '${submodule.submodule.name}':`, e);
moduleDone();
}
// clear callback, this prop is used to test if all submodule callbacks are complete below
submodule.callback = undefined;
});
}
/**
* This function will create a combined object for all subModule Ids
* @param {SubmoduleContainer[]} submodules
*/
function getCombinedSubmoduleIds(submodules) {
if (!Array.isArray(submodules) || !submodules.length) {
return {};
}
return getPrioritizedCombinedSubmoduleIds(submodules)
}
/**
* This function will return a submodule ID object for particular source name
* @param {SubmoduleContainer[]} submodules
* @param {string} sourceName
*/
function getSubmoduleId(submodules, sourceName) {
if (!Array.isArray(submodules) || !submodules.length) {
return {};
}
const prioritisedIds = getPrioritizedCombinedSubmoduleIds(submodules);
const eligibleIdName = Object.keys(prioritisedIds).find(idName => {
const config = USER_IDS_CONFIG[idName];
return config?.source === sourceName || (isFn(config?.getSource) && config.getSource() === sourceName);
});
return eligibleIdName ? {[eligibleIdName]: prioritisedIds[eligibleIdName]} : [];
}
/**
* This function will create a combined object for bidder with allowed subModule Ids
* @param {SubmoduleContainer[]} submodules
* @param {string} bidder
*/
function getCombinedSubmoduleIdsForBidder(submodules, bidder) {
if (!Array.isArray(submodules) || !submodules.length || !bidder) {
return {};
}
const eligibleSubmodules = submodules
.filter(i => !i.config.bidders || !isArray(i.config.bidders) || includes(i.config.bidders, bidder))
return getPrioritizedCombinedSubmoduleIds(eligibleSubmodules);
}
/**
* @param {SubmoduleContainer[]} submodules
*/
function getPrioritizedCombinedSubmoduleIds(submodules) {
const combinedIdStates = submodules
.filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length)
.reduce((carry, i) => {
Object.keys(i.idObj).forEach(key => {
const maybeCurrentIdPriority = idPriority[key]?.indexOf(i.submodule.name);
const currentIdPriority = isNumber(maybeCurrentIdPriority) ? maybeCurrentIdPriority : -1;
const currentIdState = {priority: currentIdPriority, value: i.idObj[key]};
if (carry[key]) {
const winnerIdState = currentIdState.priority > carry[key].priority ? currentIdState : carry[key];
carry[key] = winnerIdState;
} else {
carry[key] = currentIdState;
}
});
return carry;
}, {});
const result = {};
Object.keys(combinedIdStates).forEach(key => {
result[key] = combinedIdStates[key].value
});
return result;
}
/**
* @param {AdUnit[]} adUnits
* @param {SubmoduleContainer[]} submodules
*/
function addIdDataToAdUnitBids(adUnits, submodules) {
if ([adUnits].some(i => !Array.isArray(i) || !i.length)) {
return;
}
adUnits.forEach(adUnit => {
if (adUnit.bids && isArray(adUnit.bids)) {
adUnit.bids.forEach(bid => {
const combinedSubmoduleIds = getCombinedSubmoduleIdsForBidder(submodules, bid.bidder);
if (Object.keys(combinedSubmoduleIds).length) {
// create a User ID object on the bid,
bid.userId = combinedSubmoduleIds;
bid.userIdAsEids = createEidsArray(combinedSubmoduleIds);
}
});
}
});
}
const INIT_CANCELED = {};
function idSystemInitializer({delay = GreedyPromise.timeout} = {}) {
const startInit = defer();
const startCallbacks = defer();
let cancel;
let initialized = false;
let initMetrics;
function cancelAndTry(promise) {
initMetrics = uidMetrics().fork();
if (cancel != null) {
cancel.reject(INIT_CANCELED);
}
cancel = defer();
return GreedyPromise.race([promise, cancel.promise])
.finally(initMetrics.startTiming('userId.total'))
}
// grab a reference to global vars so that the promise chains remain isolated;
// multiple calls to `init` (from tests) might otherwise cause them to interfere with each other
let initModules = initializedSubmodules;
let allModules = submodules;
function checkRefs(fn) {
// unfortunately tests have their own global state that needs to be guarded, so even if we keep ours tidy,
// we cannot let things like submodule callbacks run (they pollute things like the global `server` XHR mock)
return function(...args) {
if (initModules === initializedSubmodules && allModules === submodules) {
return fn(...args);
}
}
}
function timeGdpr() {
return gdprDataHandler.promise.finally(initMetrics.startTiming('userId.init.gdpr'));
}
function timeGpp() {
return gppDataHandler.promise.finally(initMetrics.startTiming('userId.init.gpp'))
}
let done = cancelAndTry(
GreedyPromise.all([hooksReady, startInit.promise])
.then(() => GreedyPromise.all([timeGdpr(), timeGpp()]).then(([gdpr]) => gdpr))
.then(checkRefs((consentData) => {
initSubmodules(initModules, allModules, consentData);
}))
.then(() => startCallbacks.promise.finally(initMetrics.startTiming('userId.callbacks.pending')))
.then(checkRefs(() => {
const modWithCb = initModules.filter(item => isFn(item.callback));
if (modWithCb.length) {
return new GreedyPromise((resolve) => processSubmoduleCallbacks(modWithCb, resolve, initModules));
}
}))
);
/**
* with `ready` = true, starts initialization; with `refresh` = true, reinitialize submodules (optionally
* filtered by `submoduleNames`).
*/
return function ({refresh = false, submoduleNames = null, ready = false} = {}) {
if (ready && !initialized) {
initialized = true;
startInit.resolve();
// submodule callbacks should run immediately if `auctionDelay` > 0, or `syncDelay` ms after the
// auction ends otherwise
if (auctionDelay > 0) {
startCallbacks.resolve();
} else {
events.on(CONSTANTS.EVENTS.AUCTION_END, function auctionEndHandler() {
events.off(CONSTANTS.EVENTS.AUCTION_END, auctionEndHandler);
delay(syncDelay).then(startCallbacks.resolve);
});
}
}
if (refresh && initialized) {
done = cancelAndTry(
done
.catch(() => null)
.then(timeGdpr) // fetch again in case a refresh was forced before this was resolved
.then(checkRefs((consentData) => {
const cbModules = initSubmodules(
initModules,
allModules.filter((sm) => submoduleNames == null || submoduleNames.includes(sm.submodule.name)),
consentData,
true
).filter((sm) => {
return sm.callback != null;
});
if (cbModules.length) {
return new GreedyPromise((resolve) => processSubmoduleCallbacks(cbModules, resolve, initModules));
}
}))
);
}
return done;
};
}
let initIdSystem;
function getPPID(eids = getUserIdsAsEids() || []) {
// userSync.ppid should be one of the 'source' values in getUserIdsAsEids() eg pubcid.org or id5-sync.com
const matchingUserId = ppidSource && eids.find(userID => userID.source === ppidSource);
if (matchingUserId && typeof deepAccess(matchingUserId, 'uids.0.id') === 'string') {
const ppidValue = matchingUserId.uids[0].id.replace(/[\W_]/g, '');
if (ppidValue.length >= 32 && ppidValue.length <= 150) {
return ppidValue;
} else {
logWarn(`User ID - Googletag Publisher Provided ID for ${ppidSource} is not between 32 and 150 characters - ${ppidValue}`);
}
}
}
/**
* Hook is executed before adapters, but after consentManagement. Consent data is requied because
* this module requires GDPR consent with Purpose #1 to save data locally.
* The two main actions handled by the hook are:
* 1. check gdpr consentData and handle submodule initialization.
* 2. append user id data (loaded from cookied/html or from the getId method) to bids to be accessed in adapters.
* @param {Object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids.
* @param {function} fn required; The next function in the chain, used by hook.js
*/
export const requestBidsHook = timedAuctionHook('userId', function requestBidsHook(fn, reqBidsConfigObj, {delay = GreedyPromise.timeout, getIds = getUserIdsAsync} = {}) {
GreedyPromise.race([
getIds().catch(() => null),
delay(auctionDelay)
]).then(() => {
// pass available user id data to bid adapters
addIdDataToAdUnitBids(reqBidsConfigObj.adUnits || getGlobal().adUnits, initializedSubmodules);
uidMetrics().join(useMetrics(reqBidsConfigObj.metrics), {propagate: false, includeGroups: true});
// calling fn allows prebid to continue processing
fn.call(this, reqBidsConfigObj);
});
});
/**
* This function will be exposed in global-name-space so that userIds stored by Prebid UserId module can be used by external codes as well.
* Simple use case will be passing these UserIds to A9 wrapper solution
*/
function getUserIds() {
return getCombinedSubmoduleIds(initializedSubmodules)
}
/**
* This function will be exposed in global-name-space so that userIds stored by Prebid UserId module can be used by external codes as well.
* Simple use case will be passing these UserIds to A9 wrapper solution
*/
function getUserIdsAsEids() {
return createEidsArray(getUserIds())
}
/**
* This function will be exposed in global-name-space so that userIds stored by Prebid UserId module can be used by external codes as well.
* Simple use case will be passing these UserIds to A9 wrapper solution
*/
function getUserIdsAsEidBySource(sourceName) {
return createEidsArray(getSubmoduleId(initializedSubmodules, sourceName))[0];
}
/**
* This function will be exposed in global-name-space so that userIds for a source can be exposed
* Sample use case is exposing this function to ESP
*/
function getEncryptedEidsForSource(source, encrypt, customFunction) {
return initIdSystem().then(() => {
let eidsSignals = {};
if (isFn(customFunction)) {
logInfo(`${MODULE_NAME} - Getting encrypted signal from custom function : ${customFunction.name} & source : ${source} `);
// Publishers are expected to define a common function which will be proxy for signal function.
const customSignals = customFunction(source);
eidsSignals[source] = customSignals ? encryptSignals(customSignals) : null; // by default encrypt using base64 to avoid JSON errors
} else {
// initialize signal with eids by default
const eid = getUserIdsAsEidBySource(source);
logInfo(`${MODULE_NAME} - Getting encrypted signal for eids :${JSON.stringify(eid)}`);
if (!isEmpty(eid)) {
eidsSignals[eid.source] = encrypt === true ? encryptSignals(eid) : eid.uids[0].id; // If encryption is enabled append version (1||) and encrypt entire object
}
}
logInfo(`${MODULE_NAME} - Fetching encrypted eids: ${eidsSignals[source]}`);
return eidsSignals[source];
})
}
function encryptSignals(signals, version = 1) {
let encryptedSig = '';
switch (version) {
case 1: // Base64 Encryption
encryptedSig = typeof signals === 'object' ? window.btoa(JSON.stringify(signals)) : window.btoa(signals); // Test encryption. To be replaced with better algo
break;
default:
break;
}
return `${version}||${encryptedSig}`;
}
/**
* This function will be exposed in the global-name-space so that publisher can register the signals-ESP.
*/
function registerSignalSources() {
if (!isGptPubadsDefined()) {
return;
}
window.googletag.encryptedSignalProviders = window.googletag.encryptedSignalProviders || [];
const encryptedSignalSources = config.getConfig('userSync.encryptedSignalSources');
if (encryptedSignalSources) {
const registerDelay = encryptedSignalSources.registerDelay || 0;
setTimeout(() => {
encryptedSignalSources['sources'] && encryptedSignalSources['sources'].forEach(({ source, encrypt, customFunc }) => {
source.forEach((src) => {
window.googletag.encryptedSignalProviders.push({
id: src,
collectorFunction: () => getEncryptedEidsForSource(src, encrypt, customFunc)
});
});
})
}, registerDelay)
} else {
logWarn(`${MODULE_NAME} - ESP : encryptedSignalSources config not defined under userSync Object`);
}
}
/**
* Force (re)initialization of ID submodules.
*
* This will force a refresh of the specified ID submodules regardless of `auctionDelay` / `syncDelay` settings, and
* return a promise that resolves to the same value as `getUserIds()` when the refresh is complete.
* If a refresh is already in progress, it will be canceled (rejecting promises returned by previous calls to `refreshUserIds`).
*
* @param submoduleNames? submodules to refresh. If omitted, refresh all submodules.
* @param callback? called when the refresh is complete
*/
function refreshUserIds({submoduleNames} = {}, callback) {
return initIdSystem({refresh: true, submoduleNames})
.then(() => {
if (callback && isFn(callback)) {
callback();
}
return getUserIds();
});
}
/**
* @returns a promise that resolves to the same value as `getUserIds()`, but only once all ID submodules have completed
* initialization. This can also be used to synchronize calls to other ID accessors, e.g.
*
* ```
* pbjs.getUserIdsAsync().then(() => {
* const eids = pbjs.getUserIdsAsEids(); // guaranteed to be completely initialized at this point
* });
* ```
*/
function getUserIdsAsync() {
return initIdSystem().then(
() => getUserIds(),
(e) => {
if (e === INIT_CANCELED) {
// there's a pending refresh - because GreedyPromise runs this synchronously, we are now in the middle
// of canceling the previous init, before the refresh logic has had a chance to run.
// Use a "normal" Promise to clear the stack and let it complete (or this will just recurse infinitely)
return Promise.resolve().then(getUserIdsAsync)
} else {
logError('Error initializing userId', e)
return GreedyPromise.reject(e)
}
}
);
}
function populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh, allSubmodules) {
// There are two submodule configuration types to handle: storage or value
// 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method
// 2. value: pass directly to bids
if (submodule.config.storage) {
let storedId = getStoredValue(submodule);
let response;
let refreshNeeded = false;
if (typeof submodule.config.storage.refreshInSeconds === 'number') {
const storedDate = new Date(getStoredValue(submodule, 'last'));
refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000);
}
if (!storedId || refreshNeeded || forceRefresh || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) {
// No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule.
response = submodule.submodule.getId(submodule.config, consentData, storedId);
} else if (typeof submodule.submodule.extendId === 'function') {
// If the id exists already, give submodule a chance to decide additional actions that need to be taken
response = submodule.submodule.extendId(submodule.config, consentData, storedId);
}
if (isPlainObject(response)) {
if (response.id) {
// A getId/extendId result assumed to be valid user id data, which should be saved to users local storage or cookies
setStoredValue(submodule, response.id);
storedId = response.id;
}
if (typeof response.callback === 'function') {
// Save async callback to be invoked after auction
submodule.callback = response.callback;
}
}
if (storedId) {
// cache decoded value (this is copied to every adUnit bid)
submodule.idObj = submodule.submodule.decode(storedId, submodule.config);
}
} else if (submodule.config.value) {
// cache decoded value (this is copied to every adUnit bid)
submodule.idObj = submodule.config.value;
} else {
const response = submodule.submodule.getId(submodule.config, consentData, undefined);
if (isPlainObject(response)) {
if (typeof response.callback === 'function') { submodule.callback = response.callback; }
if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config); }
}
}
updatePPID(getCombinedSubmoduleIds(allSubmodules));
}
function updatePPID(userIds = getUserIds()) {
if (userIds && ppidSource) {
const ppid = getPPID(createEidsArray(userIds));
if (ppid) {
if (isGptPubadsDefined()) {
window.googletag.pubads().setPublisherProvidedId(ppid);
} else {
window.googletag = window.googletag || {};
window.googletag.cmd = window.googletag.cmd || [];
window.googletag.cmd.push(function() {
window.googletag.pubads().setPublisherProvidedId(ppid);
});
}
}
}
}
function initSubmodules(dest, submodules, consentData, forceRefresh = false) {
return uidMetrics().fork().measureTime('userId.init.modules', function () {
if (!submodules.length) return []; // to simplify log messages from here on
/**
* filter out submodules that:
*
* - cannot use the storage they've been set up with (storage not available / not allowed / disabled)
* - are not allowed to perform the `enrichEids` activity
*/
submodules = submodules.filter((submod) => {
return (!submod.config.storage || canUseStorage(submod)) &&
dep.isAllowed(ACTIVITY_ENRICH_EIDS, activityParams(MODULE_TYPE_UID, submod.config.name));
});
if (!submodules.length) {
logWarn(`${MODULE_NAME} - no ID module configured`);
return [];
}
// we always want the latest consentData stored, even if we don't execute any submodules
const storedConsentData = getStoredConsentData();
setStoredConsentData(consentData);
const initialized = submodules.reduce((carry, submodule) => {
return submoduleMetrics(submodule.submodule.name).measureTime('init', () => {
try {
populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh, submodules);
carry.push(submodule);
} catch (e) {
logError(`Error in userID module '${submodule.submodule.name}':`, e);
}
return carry;
})
}, []);
if (initialized.length) {
setPrebidServerEidPermissions(initialized);
}
initialized.forEach(updateInitializedSubmodules.bind(null, dest));
return initialized;
})
}
function updateInitializedSubmodules(dest, submodule) {
let updated = false;
for (let i = 0; i < dest.length; i++) {
if (submodule.config.name.toLowerCase() === dest[i].config.name.toLowerCase()) {
updated = true;
dest[i] = submodule;
break;
}
}
if (!updated) {
dest.push(submodule);
}
}
/**
* list of submodule configurations with valid 'storage' or 'value' obj definitions
* * storage config: contains values for storing/retrieving User ID data in browser storage
* * value config: object properties that are copied to bids (without saving to storage)
* @param {SubmoduleConfig[]} configRegistry
* @param {Submodule[]} submoduleRegistry
* @param {string[]} activeStorageTypes
* @returns {SubmoduleConfig[]}
*/
function getValidSubmoduleConfigs(configRegistry, submoduleRegistry) {
if (!Array.isArray(configRegistry)) {
return [];
}
return configRegistry.reduce((carry, config) => {
// every submodule config obj must contain a valid 'name'
if (!config || isEmptyStr(config.name)) {
return carry;
}
// Validate storage config contains 'type' and 'name' properties with non-empty string values
// 'type' must be one of html5, cookies
if (config.storage &&
!isEmptyStr(config.storage.type) &&
!isEmptyStr(config.storage.name) &&
ALL_STORAGE_TYPES.has(config.storage.type)) {
carry.push(config);
} else if (isPlainObject(config.value)) {
carry.push(config);
} else if (!config.storage && !config.value) {
carry.push(config);
}
return carry;
}, []);
}
const ALL_STORAGE_TYPES = new Set([LOCAL_STORAGE, COOKIE]);
function canUseStorage(submodule) {
switch (submodule.config?.storage?.type) {
case LOCAL_STORAGE:
if (submodule.storageMgr.localStorageIsEnabled()) {
if (coreStorage.getDataFromLocalStorage(PBJS_USER_ID_OPTOUT_NAME)) {
logInfo(`${MODULE_NAME} - opt-out localStorage found, storage disabled`);
return false
}
return true;
}
break;
case COOKIE:
if (submodule.storageMgr.cookiesAreEnabled()) {
if (coreStorage.getCookie(PBJS_USER_ID_OPTOUT_NAME)) {
logInfo(`${MODULE_NAME} - opt-out cookie found, storage disabled`);
return false;
}
return true
}
break;
}
return false;
}
/**