Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Topics module: Initial Topics iframe implementation #8947

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions integrationExamples/gpt/topics_frame.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Topics demo</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="favicon.ico" rel="shortcut icon">
<script>
async function getTopics() {
try {
if ('browsingTopics' in document && document.featurePolicy.allowsFeature('browsing-topics')) {
const topics = await document.browsingTopics();
console.log('Called iframe:', window.location.hostname, topics, '\nNumber of topics: ', topics.length);
return Promise.resolve(topics);
} else {
console.log('document.browsingTopics() not supported');
}

} catch (error) {
console.log('Error:', error);
}
}

document.addEventListener('DOMContentLoaded', async function () {
const topics = await getTopics()
const url = new URL(window.location.href);
const params = new URLSearchParams(url.search);
const message = JSON.stringify({
segment: {
domain: window.location.hostname,
topics,
bidder: params.get("bidder")
},
date: Date.now(),
});
window.parent.postMessage(message, '*');
pm-nitin-nimbalkar marked this conversation as resolved.
Show resolved Hide resolved
});
</script>
</head>
<body>
</body>
</html>
139 changes: 138 additions & 1 deletion modules/topicsFpdModule.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import {logError, logWarn, mergeDeep} from '../src/utils.js';
import {logError, logWarn, mergeDeep, isEmpty, safeJSONParse} from '../src/utils.js';
import {getRefererInfo} from '../src/refererDetection.js';
import {submodule} from '../src/hook.js';
import {GreedyPromise} from '../src/utils/promise.js';
import {config} from '../src/config.js';
import {getStorageManager} from '../src/storageManager.js';
import {includes} from '../src/polyfill.js';

export const storage = getStorageManager();
export const topicStorageName = 'prebid:topics';
export const lastUpdated = 'lastUpdated';

const iframeLoadedURL = [];
const TAXONOMIES = {
// map from topic taxonomyVersion to IAB segment taxonomy
'1': 600
}

const DEFAULT_EXPIRATION_DAYS = 21;

const bidderIframeList = {
maxTopicCaller: 1,
bidders: [{
bidder: 'pubmatic',
iframeURL: 'https://pubmatic.com:8080/topics/fpd/topic.html' // dummy URL for NOW
pm-nitin-nimbalkar marked this conversation as resolved.
Show resolved Hide resolved
}]
}

function partitionBy(field, items) {
return items.reduce((partitions, item) => {
const key = item[field];
Expand All @@ -17,6 +35,20 @@ function partitionBy(field, items) {
}, {});
}

/**
* function to get list of loaded Iframes calling Topics API
*/
function getLoadedIframeURL() {
return iframeLoadedURL;
}

/**
* function to set/push iframe in the list which is loaded to called topics API.
*/
function setLoadedIframeURL(url) {
return iframeLoadedURL.push(url);
}

export function getTopicsData(name, topics, taxonomies = TAXONOMIES) {
return Object.entries(partitionBy('taxonomyVersion', topics))
.filter(([taxonomyVersion]) => {
Expand Down Expand Up @@ -61,7 +93,9 @@ export function getTopics(doc = document) {
const topicsData = getTopics().then((topics) => getTopicsData(getRefererInfo().domain, topics));

export function processFpd(config, {global}, {data = topicsData} = {}) {
loadTopicsForBidders();
pm-nitin-nimbalkar marked this conversation as resolved.
Show resolved Hide resolved
return data.then((data) => {
data = [].concat(data, getCachedTopics()); // Add cached data in FPD data.
if (data.length) {
mergeDeep(global, {
user: {
Expand All @@ -73,6 +107,109 @@ export function processFpd(config, {global}, {data = topicsData} = {}) {
});
}

/**
* function to fetch the cached topic data from storage for bidders and return it
*/
function getCachedTopics() {
let cachedTopicData = [];
const topics = config.getConfig('userSync.topics') || bidderIframeList;
const bidderList = topics.bidders || [];
let storedSegments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName)));
storedSegments && storedSegments.forEach((value, cachedBidder) => {
// Check bidder exist in config for cached bidder data and then only retrieve the cached data
let isBidderConfigured = bidderList.some(({bidder}) => cachedBidder == bidder)
pm-nitin-nimbalkar marked this conversation as resolved.
Show resolved Hide resolved
if (isBidderConfigured) {
if (!isCachedDataExpired(value[lastUpdated], isBidderConfigured?.expiry || DEFAULT_EXPIRATION_DAYS)) {
Object.keys(value).forEach((segData) => {
value != lastUpdated && cachedTopicData.push(value[segData]);
})
} else {
// delete the specific bidder map from the store and store the updated maps
storedSegments.delete(cachedBidder);
storage.setDataInLocalStorage(topicStorageName, JSON.stringify([...storedSegments]));
}
}
});
return cachedTopicData;
}

/**
* Recieve messages from iframe loaded for bidders to fetch topic
* @param {MessageEvent} evt
*/
function receiveMessage(evt) {
if (evt && evt.data) {
try {
let data = safeJSONParse(evt.data);
if (includes(getLoadedIframeURL(), evt.origin) && data && data.segment && !isEmpty(data.segment.topics)) {
const {domain, topics, bidder} = data.segment;
const iframeTopicsData = getTopicsData(domain, topics)[0];
iframeTopicsData && storeInLocalStorage(bidder, iframeTopicsData);
}
} catch (err) { }
}
}

/**
Function to store Topics data recieved from iframe in storage(name: "prebid:topics")
* @param {Topics} topics
*/
export function storeInLocalStorage(bidder, topics) {
const storedSegments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName)));
if (storedSegments.has(bidder)) {
storedSegments.get(bidder)[topics['ext']['segclass']] = topics;
storedSegments.get(bidder)[lastUpdated] = new Date().getTime();
storedSegments.set(bidder, storedSegments.get(bidder));
} else {
storedSegments.set(bidder, {[topics.ext.segclass]: topics, [lastUpdated]: new Date().getTime()})
}
storage.setDataInLocalStorage(topicStorageName, JSON.stringify([...storedSegments]));
}

function isCachedDataExpired(storedTime, cacheTime) {
const _MS_PER_DAY = 1000 * 60 * 60 * 24;
const now = new Date().getTime();
const daysDifference = Math.floor((storedTime - now) / _MS_PER_DAY);
return daysDifference > cacheTime;
}

/**
* Function to get random bidders based on count passed with array of bidders
**/
function getRandomBidders(arr, count) {
return ([...arr].sort(() => 0.5 - Math.random())).slice(0, count)
}

/**
* function to add listener for message receiving from IFRAME
*/
function listenMessagesFromTopicIframe() {
window.addEventListener('message', receiveMessage, false);
}

/**
* function to load the iframes of the bidder to load the topics data
*/
function loadTopicsForBidders() {
const topics = config.getConfig('userSync.topics') || bidderIframeList;
if (topics) {
listenMessagesFromTopicIframe();
const randomBidders = getRandomBidders(topics.bidders || [], topics.maxTopicCaller || 1)
randomBidders && randomBidders.forEach(({ bidder, iframeURL }) => {
if (bidder && iframeURL) {
let ifrm = document.createElement('iframe');
ifrm.name = 'ifrm_'.concat(bidder);
ifrm.src = ''.concat(iframeURL, '?bidder=').concat(bidder);
ifrm.style.display = 'none';
setLoadedIframeURL(new URL(iframeURL).origin);
iframeURL && window.document.documentElement.appendChild(ifrm);
}
})
} else {
logWarn(`Topics config not defined under userSync Object`);
}
}

submodule('firstPartyData', {
name: 'topics',
queue: 1,
Expand Down