Skip to content

Commit bd8e538

Browse files
Feature/idnt 313 topics (prebid#623)
* IDNT-313: Backporting iframe implementations for topics in 7.25 version * merge connect id changes with latest nightly (prebid#615) --------- Co-authored-by: Nitin Nimbalkar <[email protected]>
1 parent c24e36e commit bd8e538

File tree

4 files changed

+391
-5
lines changed

4 files changed

+391
-5
lines changed

modules/topicsFpdModule.js

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,31 @@
1-
import {logError, logWarn, mergeDeep} from '../src/utils.js';
1+
import {logError, logWarn, mergeDeep, isEmpty, safeJSONParse, logInfo, hasDeviceAccess} from '../src/utils.js';
22
import {getRefererInfo} from '../src/refererDetection.js';
33
import {submodule} from '../src/hook.js';
44
import {GreedyPromise} from '../src/utils/promise.js';
5+
import {config} from '../src/config.js';
6+
import {getCoreStorageManager} from '../src/storageManager.js';
7+
import {includes} from '../src/polyfill.js';
8+
import {gdprDataHandler} from '../src/adapterManager.js';
59

10+
const MODULE_NAME = 'topicsFpd';
11+
const DEFAULT_EXPIRATION_DAYS = 21;
12+
const TCF_REQUIRED_PURPOSES = ['1', '2', '3', '4'];
13+
let HAS_GDPR_CONSENT = true;
14+
let LOAD_TOPICS_INITIALISE = false;
15+
const HAS_DEVICE_ACCESS = hasDeviceAccess();
16+
17+
const bidderIframeList = {
18+
maxTopicCaller: 1,
19+
bidders: [{
20+
bidder: 'pubmatic',
21+
iframeURL: 'https://ads.pubmatic.com/AdServer/js/topics/topics_frame.html'
22+
}]
23+
}
24+
export const coreStorage = getCoreStorageManager(MODULE_NAME);
25+
export const topicStorageName = 'prebid:topics';
26+
export const lastUpdated = 'lastUpdated';
27+
28+
const iframeLoadedURL = [];
629
const TAXONOMIES = {
730
// map from topic taxonomyVersion to IAB segment taxonomy
831
'1': 600
@@ -17,6 +40,20 @@ function partitionBy(field, items) {
1740
}, {});
1841
}
1942

43+
/**
44+
* function to get list of loaded Iframes calling Topics API
45+
*/
46+
function getLoadedIframeURL() {
47+
return iframeLoadedURL;
48+
}
49+
50+
/**
51+
* function to set/push iframe in the list which is loaded to called topics API.
52+
*/
53+
function setLoadedIframeURL(url) {
54+
return iframeLoadedURL.push(url);
55+
}
56+
2057
export function getTopicsData(name, topics, taxonomies = TAXONOMIES) {
2158
return Object.entries(partitionBy('taxonomyVersion', topics))
2259
.filter(([taxonomyVersion]) => {
@@ -61,7 +98,12 @@ export function getTopics(doc = document) {
6198
const topicsData = getTopics().then((topics) => getTopicsData(getRefererInfo().domain, topics));
6299

63100
export function processFpd(config, {global}, {data = topicsData} = {}) {
101+
if (!LOAD_TOPICS_INITIALISE) {
102+
loadTopicsForBidders();
103+
LOAD_TOPICS_INITIALISE = true;
104+
}
64105
return data.then((data) => {
106+
data = [].concat(data, getCachedTopics()); // Add cached data in FPD data.
65107
if (data.length) {
66108
mergeDeep(global, {
67109
user: {
@@ -73,6 +115,145 @@ export function processFpd(config, {global}, {data = topicsData} = {}) {
73115
});
74116
}
75117

118+
/**
119+
* function to fetch the cached topic data from storage for bidders and return it
120+
*/
121+
export function getCachedTopics() {
122+
let cachedTopicData = [];
123+
if (!HAS_GDPR_CONSENT || !HAS_DEVICE_ACCESS) {
124+
return cachedTopicData;
125+
}
126+
const topics = config.getConfig('userSync.topics') || bidderIframeList;
127+
const bidderList = topics.bidders || [];
128+
let storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName)));
129+
storedSegments && storedSegments.forEach((value, cachedBidder) => {
130+
// Check bidder exist in config for cached bidder data and then only retrieve the cached data
131+
let bidderConfigObj = bidderList.find(({bidder}) => cachedBidder == bidder)
132+
if (bidderConfigObj) {
133+
if (!isCachedDataExpired(value[lastUpdated], bidderConfigObj?.expiry || DEFAULT_EXPIRATION_DAYS)) {
134+
Object.keys(value).forEach((segData) => {
135+
segData != lastUpdated && cachedTopicData.push(value[segData]);
136+
})
137+
} else {
138+
// delete the specific bidder map from the store and store the updated maps
139+
storedSegments.delete(cachedBidder);
140+
coreStorage.setDataInLocalStorage(topicStorageName, JSON.stringify([...storedSegments]));
141+
}
142+
}
143+
});
144+
return cachedTopicData;
145+
}
146+
147+
/**
148+
* Recieve messages from iframe loaded for bidders to fetch topic
149+
* @param {MessageEvent} evt
150+
*/
151+
export function receiveMessage(evt) {
152+
if (evt && evt.data) {
153+
try {
154+
let data = safeJSONParse(evt.data);
155+
if (includes(getLoadedIframeURL(), evt.origin) && data && data.segment && !isEmpty(data.segment.topics)) {
156+
const {domain, topics, bidder} = data.segment;
157+
const iframeTopicsData = getTopicsData(domain, topics)[0];
158+
iframeTopicsData && storeInLocalStorage(bidder, iframeTopicsData);
159+
}
160+
} catch (err) { }
161+
}
162+
}
163+
164+
/**
165+
Function to store Topics data recieved from iframe in storage(name: "prebid:topics")
166+
* @param {Topics} topics
167+
*/
168+
export function storeInLocalStorage(bidder, topics) {
169+
const storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName)));
170+
if (storedSegments.has(bidder)) {
171+
storedSegments.get(bidder)[topics['ext']['segclass']] = topics;
172+
storedSegments.get(bidder)[lastUpdated] = new Date().getTime();
173+
storedSegments.set(bidder, storedSegments.get(bidder));
174+
} else {
175+
storedSegments.set(bidder, {[topics.ext.segclass]: topics, [lastUpdated]: new Date().getTime()})
176+
}
177+
coreStorage.setDataInLocalStorage(topicStorageName, JSON.stringify([...storedSegments]));
178+
}
179+
180+
function isCachedDataExpired(storedTime, cacheTime) {
181+
const _MS_PER_DAY = 1000 * 60 * 60 * 24;
182+
const currentTime = new Date().getTime();
183+
const daysDifference = Math.ceil((currentTime - storedTime) / _MS_PER_DAY);
184+
return daysDifference > cacheTime;
185+
}
186+
187+
/**
188+
* Function to get random bidders based on count passed with array of bidders
189+
**/
190+
function getRandomBidders(arr, count) {
191+
return ([...arr].sort(() => 0.5 - Math.random())).slice(0, count)
192+
}
193+
194+
/**
195+
* function to add listener for message receiving from IFRAME
196+
*/
197+
function listenMessagesFromTopicIframe() {
198+
window.addEventListener('message', receiveMessage, false);
199+
}
200+
201+
function checkTCFv2(vendorData, requiredPurposes = TCF_REQUIRED_PURPOSES) {
202+
const {gdprApplies, purpose} = vendorData;
203+
if (!gdprApplies || !purpose) {
204+
return true;
205+
}
206+
return requiredPurposes.map((purposeNo) => {
207+
const purposeConsent = purpose.consents ? purpose.consents[purposeNo] : false;
208+
if (purposeConsent) {
209+
return true;
210+
}
211+
return false;
212+
}).reduce((a, b) => a && b, true);
213+
}
214+
215+
export function hasGDPRConsent() {
216+
// Check for GDPR consent for purpose 1,2,3,4 and return false if consent has not been given
217+
const gdprConsent = gdprDataHandler.getConsentData();
218+
const hasGdpr = (gdprConsent && typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies) ? 1 : 0;
219+
const gdprConsentString = hasGdpr ? gdprConsent.consentString : '';
220+
if (hasGdpr) {
221+
if ((!gdprConsentString || gdprConsentString === '') || !gdprConsent.vendorData) {
222+
return false;
223+
}
224+
return checkTCFv2(gdprConsent.vendorData);
225+
}
226+
return true;
227+
}
228+
229+
/**
230+
* function to load the iframes of the bidder to load the topics data
231+
*/
232+
function loadTopicsForBidders() {
233+
HAS_GDPR_CONSENT = hasGDPRConsent();
234+
if (!HAS_GDPR_CONSENT || !HAS_DEVICE_ACCESS) {
235+
logInfo('Topics Module : Consent string is required to fetch the topics from third party domains.');
236+
return;
237+
}
238+
const topics = config.getConfig('userSync.topics') || bidderIframeList;
239+
if (topics) {
240+
listenMessagesFromTopicIframe();
241+
const randomBidders = getRandomBidders(topics.bidders || [], topics.maxTopicCaller || 1)
242+
randomBidders && randomBidders.forEach(({ bidder, iframeURL }) => {
243+
if (bidder && iframeURL) {
244+
let ifrm = document.createElement('iframe');
245+
ifrm.name = 'ifrm_'.concat(bidder);
246+
ifrm.src = ''.concat(iframeURL, '?bidder=').concat(bidder);
247+
ifrm.style.display = 'none';
248+
setLoadedIframeURL(new URL(iframeURL).origin);
249+
iframeURL && window.document.documentElement.appendChild(ifrm);
250+
}
251+
})
252+
} else {
253+
logWarn(`Topics config not defined under userSync Object`);
254+
}
255+
}
256+
76257
submodule('firstPartyData', {
77258
name: 'topics',
78259
queue: 1,

src/constants.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
"BID_REJECTED": "bidRejected"
9191
},
9292
"REFRESH_IDMODULES_LIST": {
93-
"PRIMARY_MODULES": ["id5Id","publinkId"],
93+
"PRIMARY_MODULES": ["id5Id","publinkId","connectId"],
9494
"SCRIPT_BASED_MODULES": ["zeotapIdPlus", "identityLink", "publinkId"]
9595
},
9696
"MODULE_PARAM_TO_UPDATE_FOR_SSO": {
@@ -100,6 +100,10 @@
100100
"publinkId": [{
101101
"key": "e",
102102
"hashType": "MD5"
103+
}],
104+
"connectId": [{
105+
"key": "he",
106+
"hashType": "SHA256"
103107
}]
104108
},
105109
"REJECTION_REASON": {
@@ -150,4 +154,4 @@
150154
"type"
151155
],
152156
"IH_LOGGER_STORAGE_KEY": "IH_LGCL_TS"
153-
}
157+
}

0 commit comments

Comments
 (0)