Skip to content

Commit

Permalink
1plusX RTD submodule: New RTD Module (prebid#8614)
Browse files Browse the repository at this point in the history
* Empty shell for 1plusX RTD submodule (#1)

* Submodule initialization & functions (init; getBidRequestData) skeletons (#2)

* Testing for init function (#3)

* Requesting Profile API for Data (#4)

* Extract PAPI response & implementation example

* Transmitting targeting data to bidder adapters

* Markdown file documentation

* Code cleaned & jsDoc completed

* Change contact email + beautify parameters table + fix type in param name

* Change customerId param type to string in doc

* Add 1plusXRtdProvider as submodule of rtdModule

* Add more tests on extractConfig amongst others

* Remove SUPPORTED_BIDDERS limitation

* Remove supported bidders from docs

* Write to site.content.data.segment.id & keep legacy support for appnexus

* Change location of googleTagServices

* Add segtax for site.content.data

* Handle audiences for appNexus by putting them in config.appnexusAuctionKeywords
  • Loading branch information
bwhisp committed Jul 14, 2022
1 parent a7d9331 commit b613039
Show file tree
Hide file tree
Showing 5 changed files with 862 additions and 1 deletion.
112 changes: 112 additions & 0 deletions integrationExamples/gpt/1plusXRtdProviderExample.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<html>

<head>

<script>
var FAILSAFE_TIMEOUT = 2000;

var adUnits = [
{
code: 'test-div',
mediaTypes: {
banner: {
sizes: [[300, 250], [300, 600], [728, 90]]
}
},
bids: [
{
bidder: 'appnexus',
params: {
placementId: 13144370
}
}
]
}
];

var pbjs = pbjs || {};
pbjs.que = pbjs.que || [];
</script>
<script src="../../build/dev/prebid.js" async></script>

<script>
var googletag = googletag || {};
var testAuctionDelay = 2000;
googletag.cmd = googletag.cmd || [];
googletag.cmd.push(function () {
googletag.pubads().disableInitialLoad();
});

pbjs.que.push(function () {
pbjs.setConfig({
debug: true,
realTimeData: {
auctionDelay: testAuctionDelay, // lower in real scenario to meet publisher spec
dataProviders: [
{
name: "1plusX",
waitForIt: true,
params: {
customerId: 'acme',
bidders: ['appnexus'],
timeout: 1000
}

}
]
}
});
pbjs.addAdUnits(adUnits);
pbjs.requestBids({ bidsBackHandler: sendAdserverRequest });
});

function sendAdserverRequest() {
if (pbjs.adserverRequestSent) return;
pbjs.adserverRequestSent = true;

googletag.cmd.push(function () {
pbjs.que.push(function () {
pbjs.setTargetingForGPTAsync();
googletag.pubads().refresh();
});
});
}

setTimeout(function () {
sendAdserverRequest();
}, FAILSAFE_TIMEOUT);
</script>

<script>
(function () {
var gads = document.createElement('script');
gads.async = true;
gads.type = 'text/javascript';
var useSSL = 'https:' == document.location.protocol;
gads.src = (useSSL ? 'https:' : 'http:') +
'//securepubads.g.doubleclick.net/tag/js/gpt.js';
var node = document.getElementsByTagName('script')[0];
node.parentNode.insertBefore(gads, node);
})();
</script>

<script>
googletag.cmd.push(function () {
googletag.defineSlot('/112115922/FL_PB_MedRect', [[300, 250], [300, 600]], 'test-div').addService(googletag.pubads());
googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
</script>
</head>

<body>
<h2>1plusX RTD Module for Prebid</h2>

<div id='test-div'>
<script>
googletag.cmd.push(function () { googletag.display('test-div'); });
</script>
</div>
</body>

</html>
3 changes: 2 additions & 1 deletion modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"dfpAdServerVideo"
],
"rtdModule": [
"1plusXRtdProvider",
"airgridRtdProvider",
"akamaiDapRtdProvider",
"blueconicRtdProvider",
Expand Down Expand Up @@ -84,4 +85,4 @@
]
}
}
}
}
251 changes: 251 additions & 0 deletions modules/1plusXRtdProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import { submodule } from '../src/hook.js';
import { config } from '../src/config.js';
import { ajax } from '../src/ajax.js';
import {
logMessage, logError,
deepAccess, mergeDeep,
isNumber, isArray, deepSetValue
} from '../src/utils.js';

// Constants
const REAL_TIME_MODULE = 'realTimeData';
const MODULE_NAME = '1plusX';
const ORTB2_NAME = '1plusX.com'
const PAPI_VERSION = 'v1.0';
const LOG_PREFIX = '[1plusX RTD Module]: ';
const LEGACY_SITE_KEYWORDS_BIDDERS = ['appnexus'];
export const segtaxes = {
// cf. https://github.com/InteractiveAdvertisingBureau/openrtb/pull/108
AUDIENCE: 526,
CONTENT: 527,
};
// Functions
/**
* Extracts the parameters for 1plusX RTD module from the config object passed at instanciation
* @param {Object} moduleConfig Config object passed to the module
* @param {Object} reqBidsConfigObj Config object for the bidders; each adapter has its own entry
* @returns {Object} Extracted configuration parameters for the module
*/
export const extractConfig = (moduleConfig, reqBidsConfigObj) => {
// CustomerId
const customerId = deepAccess(moduleConfig, 'params.customerId');
if (!customerId) {
throw new Error('Missing parameter customerId in moduleConfig');
}
// Timeout
const tempTimeout = deepAccess(moduleConfig, 'params.timeout');
const timeout = isNumber(tempTimeout) && tempTimeout > 300 ? tempTimeout : 1000;

// Bidders
const biddersTemp = deepAccess(moduleConfig, 'params.bidders');
if (!isArray(biddersTemp) || !biddersTemp.length) {
throw new Error('Missing parameter bidders in moduleConfig');
}

const adUnitBidders = reqBidsConfigObj.adUnits
.flatMap(({ bids }) => bids.map(({ bidder }) => bidder))
.filter((e, i, a) => a.indexOf(e) === i);
if (!isArray(adUnitBidders) || !adUnitBidders.length) {
throw new Error('Missing parameter bidders in bidRequestConfig');
}

const bidders = biddersTemp.filter(bidder => adUnitBidders.includes(bidder));
if (!bidders.length) {
throw new Error('No bidRequestConfig bidder found in moduleConfig bidders');
}

return { customerId, timeout, bidders };
}

/**
* Gets the URL of Profile Api from which targeting data will be fetched
* @param {Object} config
* @param {string} config.customerId
* @returns {string} URL to access 1plusX Profile API
*/
const getPapiUrl = ({ customerId }) => {
// https://[yourClientId].profiles.tagger.opecloud.com/[VERSION]/targeting?url=
const currentUrl = encodeURIComponent(window.location.href);
const papiUrl = `https://${customerId}.profiles.tagger.opecloud.com/${PAPI_VERSION}/targeting?url=${currentUrl}`;
return papiUrl;
}

/**
* Fetches targeting data. It contains the audience segments & the contextual topics
* @param {string} papiUrl URL of profile API
* @returns {Promise} Promise object resolving with data fetched from Profile API
*/
const getTargetingDataFromPapi = (papiUrl) => {
return new Promise((resolve, reject) => {
const requestOptions = {
customHeaders: {
'Accept': 'application/json'
}
}
const callbacks = {
success(responseText, response) {
resolve(JSON.parse(response.response));
},
error(error) {
reject(error);
}
};
ajax(papiUrl, callbacks, null, requestOptions)
})
}

/**
* Prepares the update for the ORTB2 object
* @param {Object} targetingData Targeting data fetched from Profile API
* @param {string[]} segments Represents the audience segments of the user
* @param {string[]} topics Represents the topics of the page
* @returns {Object} Object describing the updates to make on bidder configs
*/
export const buildOrtb2Updates = ({ segments = [], topics = [] }, bidder) => {
// Currently appnexus bidAdapter doesn't support topics in `site.content.data.segment`
// Therefore, writing them in `site.keywords` until it's supported
// Other bidAdapters do fine with `site.content.data.segment`
const writeToLegacySiteKeywords = LEGACY_SITE_KEYWORDS_BIDDERS.includes(bidder);
if (writeToLegacySiteKeywords) {
const site = {
keywords: topics.join(',')
};
return { site };
}

const userData = {
name: ORTB2_NAME,
segment: segments.map((segmentId) => ({ id: segmentId }))
};
const siteContentData = {
name: ORTB2_NAME,
segment: topics.map((topicId) => ({ id: topicId })),
ext: { segtax: segtaxes.CONTENT }
}
return { userData, siteContentData };
}

/**
* Merges the targeting data with the existing config for bidder and updates
* @param {string} bidder Bidder for which to set config
* @param {Object} ortb2Updates Updates to be applied to bidder config
* @param {Object} bidderConfigs All current bidder configs
* @returns {Object} Updated bidder config
*/
export const updateBidderConfig = (bidder, ortb2Updates, bidderConfigs) => {
const { site, siteContentData, userData } = ortb2Updates;
const bidderConfigCopy = mergeDeep({}, bidderConfigs[bidder]);

if (site) {
// Legacy : cf. comment on buildOrtb2Updates first lines
const currentSite = deepAccess(bidderConfigCopy, 'ortb2.site')
const updatedSite = mergeDeep(currentSite, site);
deepSetValue(bidderConfigCopy, 'ortb2.site', updatedSite);
}

if (siteContentData) {
const siteDataPath = 'ortb2.site.content.data';
const currentSiteContentData = deepAccess(bidderConfigCopy, siteDataPath) || [];
const updatedSiteContentData = [
...currentSiteContentData.filter(({ name }) => name != siteContentData.name),
siteContentData
];
deepSetValue(bidderConfigCopy, siteDataPath, updatedSiteContentData);
}

if (userData) {
const userDataPath = 'ortb2.user.data';
const currentUserData = deepAccess(bidderConfigCopy, userDataPath) || [];
const updatedUserData = [
...currentUserData.filter(({ name }) => name != userData.name),
userData
];
deepSetValue(bidderConfigCopy, userDataPath, updatedUserData);
}

return bidderConfigCopy;
};

const setAppnexusAudiences = (audiences) => {
config.setConfig({
appnexusAuctionKeywords: {
'1plusX': audiences,
},
});
}

/**
* Updates bidder configs with the targeting data retreived from Profile API
* @param {Object} papiResponse Response from Profile API
* @param {Object} config Module configuration
* @param {string[]} config.bidders Bidders specified in module's configuration
*/
export const setTargetingDataToConfig = (papiResponse, { bidders }) => {
const bidderConfigs = config.getBidderConfig();
const { s: segments, t: topics } = papiResponse;

for (const bidder of bidders) {
const ortb2Updates = buildOrtb2Updates({ segments, topics }, bidder);
const updatedBidderConfig = updateBidderConfig(bidder, ortb2Updates, bidderConfigs);
if (updatedBidderConfig) {
config.setBidderConfig({
bidders: [bidder],
config: updatedBidderConfig
});
}
if (bidder === 'appnexus') {
// Do the legacy stuff for appnexus with segments
setAppnexusAudiences(segments);
}
}
}

// Functions exported in submodule object
/**
* Init
* @param {Object} config Module configuration
* @param {boolean} userConsent
* @returns true
*/
const init = (config, userConsent) => {
return true;
}

/**
*
* @param {Object} reqBidsConfigObj Bid request configuration object
* @param {Function} callback Called on completion
* @param {Object} moduleConfig Configuration for 1plusX RTD module
* @param {boolean} userConsent
*/
const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => {
try {
// Get the required config
const { customerId, bidders } = extractConfig(moduleConfig, reqBidsConfigObj);
// Get PAPI URL
const papiUrl = getPapiUrl({ customerId })
// Call PAPI
getTargetingDataFromPapi(papiUrl)
.then((papiResponse) => {
logMessage(LOG_PREFIX, 'Get targeting data request successful');
setTargetingDataToConfig(papiResponse, { bidders });
callback();
})
.catch((error) => {
throw error;
})
} catch (error) {
logError(LOG_PREFIX, error);
callback();
}
}

// The RTD submodule object to be exported
export const onePlusXSubmodule = {
name: MODULE_NAME,
init,
getBidRequestData
}

// Register the onePlusXSubmodule as submodule of realTimeData
submodule(REAL_TIME_MODULE, onePlusXSubmodule);
Loading

0 comments on commit b613039

Please sign in to comment.