Skip to content

Commit b613039

Browse files
authored
1plusX RTD submodule: New RTD Module (#8614)
* 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
1 parent a7d9331 commit b613039

File tree

5 files changed

+862
-1
lines changed

5 files changed

+862
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<html>
2+
3+
<head>
4+
5+
<script>
6+
var FAILSAFE_TIMEOUT = 2000;
7+
8+
var adUnits = [
9+
{
10+
code: 'test-div',
11+
mediaTypes: {
12+
banner: {
13+
sizes: [[300, 250], [300, 600], [728, 90]]
14+
}
15+
},
16+
bids: [
17+
{
18+
bidder: 'appnexus',
19+
params: {
20+
placementId: 13144370
21+
}
22+
}
23+
]
24+
}
25+
];
26+
27+
var pbjs = pbjs || {};
28+
pbjs.que = pbjs.que || [];
29+
</script>
30+
<script src="../../build/dev/prebid.js" async></script>
31+
32+
<script>
33+
var googletag = googletag || {};
34+
var testAuctionDelay = 2000;
35+
googletag.cmd = googletag.cmd || [];
36+
googletag.cmd.push(function () {
37+
googletag.pubads().disableInitialLoad();
38+
});
39+
40+
pbjs.que.push(function () {
41+
pbjs.setConfig({
42+
debug: true,
43+
realTimeData: {
44+
auctionDelay: testAuctionDelay, // lower in real scenario to meet publisher spec
45+
dataProviders: [
46+
{
47+
name: "1plusX",
48+
waitForIt: true,
49+
params: {
50+
customerId: 'acme',
51+
bidders: ['appnexus'],
52+
timeout: 1000
53+
}
54+
55+
}
56+
]
57+
}
58+
});
59+
pbjs.addAdUnits(adUnits);
60+
pbjs.requestBids({ bidsBackHandler: sendAdserverRequest });
61+
});
62+
63+
function sendAdserverRequest() {
64+
if (pbjs.adserverRequestSent) return;
65+
pbjs.adserverRequestSent = true;
66+
67+
googletag.cmd.push(function () {
68+
pbjs.que.push(function () {
69+
pbjs.setTargetingForGPTAsync();
70+
googletag.pubads().refresh();
71+
});
72+
});
73+
}
74+
75+
setTimeout(function () {
76+
sendAdserverRequest();
77+
}, FAILSAFE_TIMEOUT);
78+
</script>
79+
80+
<script>
81+
(function () {
82+
var gads = document.createElement('script');
83+
gads.async = true;
84+
gads.type = 'text/javascript';
85+
var useSSL = 'https:' == document.location.protocol;
86+
gads.src = (useSSL ? 'https:' : 'http:') +
87+
'//securepubads.g.doubleclick.net/tag/js/gpt.js';
88+
var node = document.getElementsByTagName('script')[0];
89+
node.parentNode.insertBefore(gads, node);
90+
})();
91+
</script>
92+
93+
<script>
94+
googletag.cmd.push(function () {
95+
googletag.defineSlot('/112115922/FL_PB_MedRect', [[300, 250], [300, 600]], 'test-div').addService(googletag.pubads());
96+
googletag.pubads().enableSingleRequest();
97+
googletag.enableServices();
98+
});
99+
</script>
100+
</head>
101+
102+
<body>
103+
<h2>1plusX RTD Module for Prebid</h2>
104+
105+
<div id='test-div'>
106+
<script>
107+
googletag.cmd.push(function () { googletag.display('test-div'); });
108+
</script>
109+
</div>
110+
</body>
111+
112+
</html>

modules/.submodules.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"dfpAdServerVideo"
5050
],
5151
"rtdModule": [
52+
"1plusXRtdProvider",
5253
"airgridRtdProvider",
5354
"akamaiDapRtdProvider",
5455
"blueconicRtdProvider",
@@ -84,4 +85,4 @@
8485
]
8586
}
8687
}
87-
}
88+
}

modules/1plusXRtdProvider.js

+251
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { submodule } from '../src/hook.js';
2+
import { config } from '../src/config.js';
3+
import { ajax } from '../src/ajax.js';
4+
import {
5+
logMessage, logError,
6+
deepAccess, mergeDeep,
7+
isNumber, isArray, deepSetValue
8+
} from '../src/utils.js';
9+
10+
// Constants
11+
const REAL_TIME_MODULE = 'realTimeData';
12+
const MODULE_NAME = '1plusX';
13+
const ORTB2_NAME = '1plusX.com'
14+
const PAPI_VERSION = 'v1.0';
15+
const LOG_PREFIX = '[1plusX RTD Module]: ';
16+
const LEGACY_SITE_KEYWORDS_BIDDERS = ['appnexus'];
17+
export const segtaxes = {
18+
// cf. https://github.com/InteractiveAdvertisingBureau/openrtb/pull/108
19+
AUDIENCE: 526,
20+
CONTENT: 527,
21+
};
22+
// Functions
23+
/**
24+
* Extracts the parameters for 1plusX RTD module from the config object passed at instanciation
25+
* @param {Object} moduleConfig Config object passed to the module
26+
* @param {Object} reqBidsConfigObj Config object for the bidders; each adapter has its own entry
27+
* @returns {Object} Extracted configuration parameters for the module
28+
*/
29+
export const extractConfig = (moduleConfig, reqBidsConfigObj) => {
30+
// CustomerId
31+
const customerId = deepAccess(moduleConfig, 'params.customerId');
32+
if (!customerId) {
33+
throw new Error('Missing parameter customerId in moduleConfig');
34+
}
35+
// Timeout
36+
const tempTimeout = deepAccess(moduleConfig, 'params.timeout');
37+
const timeout = isNumber(tempTimeout) && tempTimeout > 300 ? tempTimeout : 1000;
38+
39+
// Bidders
40+
const biddersTemp = deepAccess(moduleConfig, 'params.bidders');
41+
if (!isArray(biddersTemp) || !biddersTemp.length) {
42+
throw new Error('Missing parameter bidders in moduleConfig');
43+
}
44+
45+
const adUnitBidders = reqBidsConfigObj.adUnits
46+
.flatMap(({ bids }) => bids.map(({ bidder }) => bidder))
47+
.filter((e, i, a) => a.indexOf(e) === i);
48+
if (!isArray(adUnitBidders) || !adUnitBidders.length) {
49+
throw new Error('Missing parameter bidders in bidRequestConfig');
50+
}
51+
52+
const bidders = biddersTemp.filter(bidder => adUnitBidders.includes(bidder));
53+
if (!bidders.length) {
54+
throw new Error('No bidRequestConfig bidder found in moduleConfig bidders');
55+
}
56+
57+
return { customerId, timeout, bidders };
58+
}
59+
60+
/**
61+
* Gets the URL of Profile Api from which targeting data will be fetched
62+
* @param {Object} config
63+
* @param {string} config.customerId
64+
* @returns {string} URL to access 1plusX Profile API
65+
*/
66+
const getPapiUrl = ({ customerId }) => {
67+
// https://[yourClientId].profiles.tagger.opecloud.com/[VERSION]/targeting?url=
68+
const currentUrl = encodeURIComponent(window.location.href);
69+
const papiUrl = `https://${customerId}.profiles.tagger.opecloud.com/${PAPI_VERSION}/targeting?url=${currentUrl}`;
70+
return papiUrl;
71+
}
72+
73+
/**
74+
* Fetches targeting data. It contains the audience segments & the contextual topics
75+
* @param {string} papiUrl URL of profile API
76+
* @returns {Promise} Promise object resolving with data fetched from Profile API
77+
*/
78+
const getTargetingDataFromPapi = (papiUrl) => {
79+
return new Promise((resolve, reject) => {
80+
const requestOptions = {
81+
customHeaders: {
82+
'Accept': 'application/json'
83+
}
84+
}
85+
const callbacks = {
86+
success(responseText, response) {
87+
resolve(JSON.parse(response.response));
88+
},
89+
error(error) {
90+
reject(error);
91+
}
92+
};
93+
ajax(papiUrl, callbacks, null, requestOptions)
94+
})
95+
}
96+
97+
/**
98+
* Prepares the update for the ORTB2 object
99+
* @param {Object} targetingData Targeting data fetched from Profile API
100+
* @param {string[]} segments Represents the audience segments of the user
101+
* @param {string[]} topics Represents the topics of the page
102+
* @returns {Object} Object describing the updates to make on bidder configs
103+
*/
104+
export const buildOrtb2Updates = ({ segments = [], topics = [] }, bidder) => {
105+
// Currently appnexus bidAdapter doesn't support topics in `site.content.data.segment`
106+
// Therefore, writing them in `site.keywords` until it's supported
107+
// Other bidAdapters do fine with `site.content.data.segment`
108+
const writeToLegacySiteKeywords = LEGACY_SITE_KEYWORDS_BIDDERS.includes(bidder);
109+
if (writeToLegacySiteKeywords) {
110+
const site = {
111+
keywords: topics.join(',')
112+
};
113+
return { site };
114+
}
115+
116+
const userData = {
117+
name: ORTB2_NAME,
118+
segment: segments.map((segmentId) => ({ id: segmentId }))
119+
};
120+
const siteContentData = {
121+
name: ORTB2_NAME,
122+
segment: topics.map((topicId) => ({ id: topicId })),
123+
ext: { segtax: segtaxes.CONTENT }
124+
}
125+
return { userData, siteContentData };
126+
}
127+
128+
/**
129+
* Merges the targeting data with the existing config for bidder and updates
130+
* @param {string} bidder Bidder for which to set config
131+
* @param {Object} ortb2Updates Updates to be applied to bidder config
132+
* @param {Object} bidderConfigs All current bidder configs
133+
* @returns {Object} Updated bidder config
134+
*/
135+
export const updateBidderConfig = (bidder, ortb2Updates, bidderConfigs) => {
136+
const { site, siteContentData, userData } = ortb2Updates;
137+
const bidderConfigCopy = mergeDeep({}, bidderConfigs[bidder]);
138+
139+
if (site) {
140+
// Legacy : cf. comment on buildOrtb2Updates first lines
141+
const currentSite = deepAccess(bidderConfigCopy, 'ortb2.site')
142+
const updatedSite = mergeDeep(currentSite, site);
143+
deepSetValue(bidderConfigCopy, 'ortb2.site', updatedSite);
144+
}
145+
146+
if (siteContentData) {
147+
const siteDataPath = 'ortb2.site.content.data';
148+
const currentSiteContentData = deepAccess(bidderConfigCopy, siteDataPath) || [];
149+
const updatedSiteContentData = [
150+
...currentSiteContentData.filter(({ name }) => name != siteContentData.name),
151+
siteContentData
152+
];
153+
deepSetValue(bidderConfigCopy, siteDataPath, updatedSiteContentData);
154+
}
155+
156+
if (userData) {
157+
const userDataPath = 'ortb2.user.data';
158+
const currentUserData = deepAccess(bidderConfigCopy, userDataPath) || [];
159+
const updatedUserData = [
160+
...currentUserData.filter(({ name }) => name != userData.name),
161+
userData
162+
];
163+
deepSetValue(bidderConfigCopy, userDataPath, updatedUserData);
164+
}
165+
166+
return bidderConfigCopy;
167+
};
168+
169+
const setAppnexusAudiences = (audiences) => {
170+
config.setConfig({
171+
appnexusAuctionKeywords: {
172+
'1plusX': audiences,
173+
},
174+
});
175+
}
176+
177+
/**
178+
* Updates bidder configs with the targeting data retreived from Profile API
179+
* @param {Object} papiResponse Response from Profile API
180+
* @param {Object} config Module configuration
181+
* @param {string[]} config.bidders Bidders specified in module's configuration
182+
*/
183+
export const setTargetingDataToConfig = (papiResponse, { bidders }) => {
184+
const bidderConfigs = config.getBidderConfig();
185+
const { s: segments, t: topics } = papiResponse;
186+
187+
for (const bidder of bidders) {
188+
const ortb2Updates = buildOrtb2Updates({ segments, topics }, bidder);
189+
const updatedBidderConfig = updateBidderConfig(bidder, ortb2Updates, bidderConfigs);
190+
if (updatedBidderConfig) {
191+
config.setBidderConfig({
192+
bidders: [bidder],
193+
config: updatedBidderConfig
194+
});
195+
}
196+
if (bidder === 'appnexus') {
197+
// Do the legacy stuff for appnexus with segments
198+
setAppnexusAudiences(segments);
199+
}
200+
}
201+
}
202+
203+
// Functions exported in submodule object
204+
/**
205+
* Init
206+
* @param {Object} config Module configuration
207+
* @param {boolean} userConsent
208+
* @returns true
209+
*/
210+
const init = (config, userConsent) => {
211+
return true;
212+
}
213+
214+
/**
215+
*
216+
* @param {Object} reqBidsConfigObj Bid request configuration object
217+
* @param {Function} callback Called on completion
218+
* @param {Object} moduleConfig Configuration for 1plusX RTD module
219+
* @param {boolean} userConsent
220+
*/
221+
const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => {
222+
try {
223+
// Get the required config
224+
const { customerId, bidders } = extractConfig(moduleConfig, reqBidsConfigObj);
225+
// Get PAPI URL
226+
const papiUrl = getPapiUrl({ customerId })
227+
// Call PAPI
228+
getTargetingDataFromPapi(papiUrl)
229+
.then((papiResponse) => {
230+
logMessage(LOG_PREFIX, 'Get targeting data request successful');
231+
setTargetingDataToConfig(papiResponse, { bidders });
232+
callback();
233+
})
234+
.catch((error) => {
235+
throw error;
236+
})
237+
} catch (error) {
238+
logError(LOG_PREFIX, error);
239+
callback();
240+
}
241+
}
242+
243+
// The RTD submodule object to be exported
244+
export const onePlusXSubmodule = {
245+
name: MODULE_NAME,
246+
init,
247+
getBidRequestData
248+
}
249+
250+
// Register the onePlusXSubmodule as submodule of realTimeData
251+
submodule(REAL_TIME_MODULE, onePlusXSubmodule);

0 commit comments

Comments
 (0)