Skip to content

Commit f2cb574

Browse files
Merge pull request prebid#622 from PubMatic-OpenWrap/js_28_feb
WIP JS changes for on demand release
2 parents ed980f2 + 10eb8bc commit f2cb574

File tree

5 files changed

+410
-6
lines changed

5 files changed

+410
-6
lines changed

modules/pubmaticBidAdapter.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,9 @@ export function prepareMetaObject(br, bid, seat) {
10211021
// if (bid.ext.advertiserName) br.meta.advertiserName = bid.ext.advertiserName;
10221022
// if (bid.ext.agencyName) br.meta.agencyName = bid.ext.agencyName;
10231023
// if (bid.ext.brandName) br.meta.brandName = bid.ext.brandName;
1024-
// if (bid.ext.dchain) br.meta.dchain = bid.ext.dchain;
1024+
if (bid.ext && bid.ext.dchain) {
1025+
br.meta.dchain = bid.ext.dchain;
1026+
}
10251027

10261028
const advid = seat || (bid.ext && bid.ext.advid);
10271029
if (advid) {
@@ -1274,6 +1276,11 @@ export const spec = {
12741276
blockedIabCategories = blockedIabCategories.concat(commonFpd.bcat);
12751277
}
12761278

1279+
// check if fpd ortb2 contains device property with sua object
1280+
if (commonFpd.device?.sua) {
1281+
payload.device.sua = commonFpd.device.sua;
1282+
}
1283+
12771284
if (commonFpd.ext?.prebid?.bidderparams?.[bidderRequest.bidderCode]?.acat) {
12781285
const acatParams = commonFpd.ext.prebid.bidderparams[bidderRequest.bidderCode].acat;
12791286
_allowedIabCategoriesValidation(payload, acatParams);

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,

test/spec/modules/pubmaticBidAdapter_spec.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2638,6 +2638,21 @@ describe('PubMatic adapter', function () {
26382638
expect(data.imp[0]['video']['h']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[1]);
26392639
});
26402640

2641+
it('should pass device.sua if present in bidderRequest fpd ortb2 object', function () {
2642+
const suaObject = {'source': 2, 'platform': {'brand': 'macOS', 'version': ['12', '4', '0']}, 'browsers': [{'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']}], 'mobile': 0, 'model': '', 'bitness': '64', 'architecture': 'x86'};
2643+
let request = spec.buildRequests(multipleMediaRequests, {
2644+
auctionId: 'new-auction-id',
2645+
ortb2: {
2646+
device: {
2647+
sua: suaObject
2648+
}
2649+
}
2650+
});
2651+
let data = JSON.parse(request.data);
2652+
expect(data.device.sua).to.exist.and.to.be.an('object');
2653+
expect(data.device.sua).to.deep.equal(suaObject);
2654+
});
2655+
26412656
it('Request params check for 1 banner and 1 video ad', function () {
26422657
let request = spec.buildRequests(multipleMediaRequests, {
26432658
auctionId: 'new-auction-id'
@@ -4631,7 +4646,7 @@ describe('PubMatic adapter', function () {
46314646
// agencyName: 'agnm',
46324647
// brandId: 'brid',
46334648
// brandName: 'brnm',
4634-
// dchain: 'dc',
4649+
dchain: 'dc',
46354650
// demandSource: 'ds',
46364651
// secondaryCatIds: ['secondaryCatIds']
46374652
}
@@ -4649,7 +4664,7 @@ describe('PubMatic adapter', function () {
46494664
// expect(br.meta.agencyName).to.equal('agnm');
46504665
expect(br.meta.brandId).to.equal('mystartab.com');
46514666
// expect(br.meta.brandName).to.equal('brnm');
4652-
// expect(br.meta.dchain).to.equal('dc');
4667+
expect(br.meta.dchain).to.equal('dc');
46534668
expect(br.meta.demandSource).to.equal(6);
46544669
expect(br.meta.secondaryCatIds).to.be.an('array').with.length.above(0);
46554670
expect(br.meta.secondaryCatIds[0]).to.equal('IAB_CATEGORY');

0 commit comments

Comments
 (0)