Skip to content

Commit

Permalink
Neuwo RTD Module : initial release (prebid#9385)
Browse files Browse the repository at this point in the history
* added neuwoRtdProvider.js (implementation), integration test spec, initial description document (neuwoRtdProvider.md)

* neuwoRtdProvider finish: added _example.html using https://docs.prebid.org/dev-docs/examples/basic-example.html
improved neuwoRtdProvider logging and error handling
improved neuwoRtdProvider.md testing steps

* neuwoRtdProvider.js -> updated to convert response using IAB to Tax ID conversion dictionary
+tests, finished "no response" test

* neuwoRtdProvider.js: added BILLABLE_EVENTs: 'auction' on getBidRequestData, 'request' on successful parse of neuwo request, 'general' on if global data actually extended using topics

* review reflections: added injection into site.cattax, site.pagecat for "IAB 2.2", clarified conversion code conversion dictionary comment to include IAB 2+ - added tests to further ensure endpoint response malformation handling robustness

* neuwoRtdProvider.js -> updated segtax code to compatible non-deprecated version (2.0 -> to 2.2, used conversion table is the same)

* neuwoRtdProvider.js -> reduced:
- amount and mutability of variables (const)
- responsibility of injectTopics, callback lifted into callbacks of the ajax call
- amount of billable events, on api call success only (expects marketing_categories)
  • Loading branch information
moquity authored and JacobKlein26 committed Feb 8, 2023
1 parent c615cbb commit 3195179
Show file tree
Hide file tree
Showing 4 changed files with 524 additions and 0 deletions.
190 changes: 190 additions & 0 deletions integrationExamples/gpt/neuwoRtdProvider_example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name=viewport content="width=device-width, initial-scale=1">
<script src="../../build/dev/prebid.js" async type="text/javascript"></script>
<script async src="//www.googletagservices.com/tag/js/gpt.js"></script>
<script>
var helper = document.getElementById('need-help');
if (helper) helper.style.display = location.href !== 'http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html' ? 'block' : 'none';

/* adapted from https://docs.prebid.org/dev-docs/examples/basic-example.html */
var div_1_sizes = [
[300, 250],
[300, 600]
];
var div_2_sizes = [
[728, 90],
[970, 250]
];
var PREBID_TIMEOUT = 1000;
var FAILSAFE_TIMEOUT = 3000;

var adUnits = [
{
code: '/19968336/header-bid-tag-0',
mediaTypes: {
banner: {
sizes: div_1_sizes
}
},
bids: [{
bidder: 'appnexus',
params: {
placementId: 13144370
}
}]
},
{
code: '/19968336/header-bid-tag-1',
mediaTypes: {
banner: {
sizes: div_2_sizes
}
},
bids: [{
bidder: 'appnexus',
params: {
placementId: 13144370
}
}]
}
];

var pbjs = pbjs || {};
pbjs.que = pbjs.que || [];

var googletag = googletag || {};
googletag.cmd = googletag.cmd || [];

// DYNAMIC CONFIGURATION BLOCK
function requestBids() {
pbjs.que.push(function() {
// send requests
pbjs.addAdUnits(adUnits);
pbjs.requestBids({
bidsBackHandler: initAdserver,
timeout: PREBID_TIMEOUT
});
})
}

function timeout() {
setTimeout(function() {
initAdserver();
}, FAILSAFE_TIMEOUT);
}

function requestDisplay() {
googletag.cmd.push(function() {
googletag.display('div-2');
});

googletag.cmd.push(function() {
googletag.display('div-1');
});
}

function onSettingsUpdate() {
let fieldElem = document.getElementById('token-to-use');
let publicToken = fieldElem ? fieldElem.value : '';
if (!publicToken) {
alert('please enter your token for Neuwo AI API on the field');
if (fieldElem) fieldElem.focus();
return;
}
let urlElem = document.getElementById('url-to-use');
let argUrl = urlElem ? urlElem.value : undefined;
pbjs.que.push(function() {
pbjs.setConfig({
debug: true,
realTimeData: {
dataProviders: [
{
name: "NeuwoRTDModule",
waitForIt: true,
params: {
publicToken,
argUrl
}
}
]
}
})
})

// trigger the bidding process, timeout handling and ad display

requestBids();
timeout();
requestDisplay();
}
// DYNAMIC CONFIGURATION BLOCK END
googletag.cmd.push(function() {
googletag.pubads().disableInitialLoad();
});

function initAdserver() {
if (pbjs.initAdserverSet) return;
pbjs.initAdserverSet = true;
googletag.cmd.push(function() {
pbjs.que.push(function() {
pbjs.setTargetingForGPTAsync();
googletag.pubads().refresh();
});
});
}

googletag.cmd.push(function() {
googletag.defineSlot('/19968336/header-bid-tag-0', div_1_sizes, 'div-1').addService(googletag.pubads());
googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
googletag.cmd.push(function() {
googletag.defineSlot('/19968336/header-bid-tag-1', div_2_sizes, 'div-2').addService(googletag.pubads());
googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
</script>
</head>
<body>
<h2>Basic Prebid.js Example using neuwoRtdProvider</h2>
<div id="need-help" style="padding: 1em; border: 2px solid gray; background-color: rgb(255, 218, 218); white-space: pre-line;">
Looks like you're not following the testing environment setup, try accessing <a href="http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html">http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html</a>
after running commands in the prebid.js source folder that includes libraries/modules/neuwoRtdProvider.js
<code style="display: block; white-space: pre-line;">
npm ci
npm i -g gulp-cli
gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter
</code>
</div>
<div>
<p>Add token and url to use for Neuwo extension configuration</p>
<input type="text" placeholder="Token for Neuwo API" id="token-to-use" />
<input type="text" placeholder="Use url for site" id="url-to-use" />
<button onClick="onSettingsUpdate()">Update</button>
</div>

<h5>Div-1</h5>
<div id='div-1'>
Ad spot div-1: This content will be replaced by prebid.js and/or related components once you click "Update"
</div>

<br>

<h5>Div-2</h5>
<div id='div-2'>
Ad spot div-2: Replaces this text as well, if everything goes to plan
<!-- example of an ad field trigger content below -->
<!--
<script type="text/javascript">
googletag.cmd.push(function() {
googletag.display('div-2');
});
</script>
-->
</div>

</body>
</html>
171 changes: 171 additions & 0 deletions modules/neuwoRtdProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { deepAccess, deepSetValue, generateUUID, logError, logInfo, mergeDeep } from '../src/utils.js';
import { getRefererInfo } from '../src/refererDetection.js';
import { ajax } from '../src/ajax.js';
import { submodule } from '../src/hook.js';
import * as events from '../src/events.js';
import CONSTANTS from '../src/constants.json';

export const DATA_PROVIDER = 'neuwo.ai';
const SEGTAX_IAB = 6 // IAB - Content Taxonomy version 2
const CATTAX_IAB = 6 // IAB Tech Lab Content Taxonomy 2.2
const RESPONSE_IAB_TIER_1 = 'marketing_categories.iab_tier_1'
const RESPONSE_IAB_TIER_2 = 'marketing_categories.iab_tier_2'

function init(config = {}, userConsent = '') {
config.params = config.params || {}
// ignore module if publicToken is missing (module setup failure)
if (!config.params.publicToken) {
logError('publicToken missing', 'NeuwoRTDModule', 'config.params.publicToken')
return false;
}
return true;
}

export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) {
config.params = config.params || {};
logInfo('NeuwoRTDModule', 'starting getBidRequestData')

const wrappedArgUrl = encodeURIComponent(config.params.argUrl || getRefererInfo().page);
const url = 'https://m1apidev.neuwo.ai/edge/GetAiTopics?' + [
'token=' + config.params.publicToken,
'lang=en',
'url=' + wrappedArgUrl
].join('&')
const billingId = generateUUID();

const success = (responseContent) => {
logInfo('NeuwoRTDModule', 'GetAiTopics: response', responseContent)
try {
const jsonContent = JSON.parse(responseContent);
if (jsonContent.marketing_categories) {
events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { type: 'request', billingId, vendor: neuwoRtdModule.name })
}
injectTopics(jsonContent, reqBidsConfigObj, billingId)
} catch (ex) {
logError('NeuwoRTDModule', 'Response to JSON parse error', ex)
}
callback()
}

const error = (err) => {
logError('xhr error', null, err);
callback()
}

ajax(url, {success, error}, null, {
// could assume Origin header is set, or
// customHeaders: { 'Origin': 'Origin' }
})
}

export function addFragment(base, path, addition) {
const container = {}
deepSetValue(container, path, addition)
mergeDeep(base, container)
}

/**
* Concatenate a base array and an array within an object
* non-array bases will be arrays, non-arrays at object key will be discarded
* @param {array} base base array to add to
* @param {object} source object to get an array from
* @param {string} key dot-notated path to array within object
* @returns base + source[key] if that's an array
*/
function combineArray(base, source, key) {
if (Array.isArray(base) === false) base = []
const addition = deepAccess(source, key, [])
if (Array.isArray(addition)) return base.concat(addition)
else return base
}

export function injectTopics(topics, bidsConfig) {
topics = topics || {}

// join arrays of IAB category details to single array
const combinedTiers = combineArray(
combineArray([], topics, RESPONSE_IAB_TIER_1),
topics, RESPONSE_IAB_TIER_2)

const segment = pickSegments(combinedTiers)
// effectively gets topics.marketing_categories.iab_tier_1, topics.marketing_categories.iab_tier_2
// used as FPD segments content

const IABSegments = {
name: DATA_PROVIDER,
ext: { segtax: SEGTAX_IAB },
segment
}

addFragment(bidsConfig.ortb2Fragments.global, 'site.content.data', [IABSegments])

// upgrade category taxonomy to IAB 2.2, inject result to page categories
if (segment.length > 0) {
addFragment(bidsConfig.ortb2Fragments.global, 'site.cattax', CATTAX_IAB)
addFragment(bidsConfig.ortb2Fragments.global, 'site.pagecat', segment.map(s => s.id))
}

logInfo('NeuwoRTDModule', 'injectTopics: post-injection bidsConfig', bidsConfig)
}

/* eslint-disable object-property-newline */
const D_IAB_ID = { // Content Taxonomy version 2.0 final release November 2017 [sic] (Taxonomy ID Mapping, IAB versions 2.0 - 2.2)
'IAB19-1': '603', 'IAB6-1': '193', 'IAB5-2': '133', 'IAB20-1': '665', 'IAB20-2': '656', 'IAB23-2': '454', 'IAB3-2': '102', 'IAB20-3': '672', 'IAB8-5': '211',
'IAB8-18': '211', 'IAB7-4': '288', 'IAB7-5': '233', 'IAB17-12': '484', 'IAB19-3': '608', 'IAB21-1': '442', 'IAB9-2': '248', 'IAB15-1': '456', 'IAB9-17': '265', 'IAB20-4': '658',
'IAB2-3': '30', 'IAB2-1': '32', 'IAB17-1': '518', 'IAB2-2': '34', 'IAB2': '1', 'IAB8-2': '215', 'IAB17-2': '545', 'IAB17-26': '547', 'IAB9-3': '249', 'IAB18-1': '553', 'IAB20-5': '674',
'IAB15-2': '465', 'IAB3-3': '119', 'IAB16-2': '423', 'IAB9-4': '259', 'IAB9-5': '270', 'IAB18-2': '574', 'IAB17-4': '549', 'IAB7-33': '312', 'IAB1-1': '42', 'IAB17-5': '485', 'IAB23-3': '458',
'IAB20-6': '675', 'IAB3': '53', 'IAB20-7': '676', 'IAB19-5': '633', 'IAB20-9': '677', 'IAB9-6': '250', 'IAB17-6': '499', 'IAB2-4': '25', 'IAB9-7': '271', 'IAB4-11': '125', 'IAB4-1': '126',
'IAB4': '123', 'IAB16-3': '424', 'IAB2-5': '18', 'IAB17-7': '486', 'IAB15-3': '466', 'IAB23-5': '459', 'IAB9-9': '260', 'IAB2-22': '19', 'IAB17-8': '500', 'IAB9-10': '261', 'IAB5-5': '137',
'IAB9-11': '262', 'IAB2-21': '3', 'IAB19-2': '610', 'IAB19-8': '600', 'IAB19-9': '601', 'IAB3-5': '121', 'IAB9-15': '264', 'IAB2-6': '8', 'IAB2-7': '9', 'IAB22-2': '474', 'IAB17-9': '491',
'IAB2-8': '10', 'IAB20-12': '678', 'IAB17-3': '492', 'IAB19-12': '611', 'IAB14-1': '188', 'IAB6-3': '194', 'IAB7-17': '316', 'IAB19-13': '612', 'IAB8-8': '217', 'IAB9-1': '205', 'IAB19-22': '613',
'IAB8-9': '218', 'IAB14-2': '189', 'IAB16-4': '425', 'IAB9-12': '251', 'IAB5': '132', 'IAB6-9': '190', 'IAB19-15': '623', 'IAB17-17': '496', 'IAB20-14': '659', 'IAB6': '186', 'IAB20-26': '666',
'IAB17-10': '510', 'IAB13-4': '396', 'IAB1-3': '201', 'IAB16-1': '426', 'IAB17-11': '511', 'IAB17-13': '511', 'IAB17-32': '511', 'IAB7-1': '225', 'IAB8': '210', 'IAB8-10': '219', 'IAB9-13': '266',
'IAB10-4': '275', 'IAB9-14': '273', 'IAB15-8': '469', 'IAB15-4': '470', 'IAB17-15': '512', 'IAB3-7': '77', 'IAB19-16': '614', 'IAB3-8': '78', 'IAB2-10': '22', 'IAB2-12': '22', 'IAB2-11': '11',
'IAB8-12': '221', 'IAB7-35': '223', 'IAB7-38': '223', 'IAB7-24': '296', 'IAB13-5': '411', 'IAB7-25': '234', 'IAB23-6': '460', 'IAB9': '239', 'IAB7-26': '235', 'IAB10': '274', 'IAB10-1': '278',
'IAB10-2': '279', 'IAB19-17': '634', 'IAB10-5': '280', 'IAB5-10': '145', 'IAB5-11': '146', 'IAB20-17': '667', 'IAB17-16': '497', 'IAB20-18': '668', 'IAB3-9': '55', 'IAB1-4': '440', 'IAB17-18': '514',
'IAB17-27': '515', 'IAB10-3': '282', 'IAB19-25': '618', 'IAB17-19': '516', 'IAB13-6': '398', 'IAB10-7': '283', 'IAB12-1': '382', 'IAB19-24': '624', 'IAB6-4': '195', 'IAB23-7': '461', 'IAB9-19': '252',
'IAB4-4': '128', 'IAB4-5': '127', 'IAB23-8': '462', 'IAB10-8': '284', 'IAB5-8': '147', 'IAB16-5': '427', 'IAB11-2': '383', 'IAB12-3': '384', 'IAB3-10': '57', 'IAB2-13': '23', 'IAB9-20': '241',
'IAB3-1': '58', 'IAB3-11': '58', 'IAB14-4': '191', 'IAB17-20': '520', 'IAB7-31': '228', 'IAB7-37': '301', 'IAB3-12': '107', 'IAB2-14': '13', 'IAB17-25': '519', 'IAB2-15': '27', 'IAB1-5': '324',
'IAB1-6': '338', 'IAB9-16': '243', 'IAB13-8': '412', 'IAB12-2': '385', 'IAB9-21': '253', 'IAB8-6': '222', 'IAB7-32': '229', 'IAB2-16': '14', 'IAB17-23': '521', 'IAB13-9': '413', 'IAB17-24': '501',
'IAB9-22': '254', 'IAB15-5': '244', 'IAB6-2': '196', 'IAB6-5': '197', 'IAB6-6': '198', 'IAB2-17': '24', 'IAB13-2': '405', 'IAB13': '391', 'IAB13-7': '410', 'IAB13-12': '415', 'IAB16': '422',
'IAB9-23': '255', 'IAB7-36': '236', 'IAB15-6': '471', 'IAB2-18': '15', 'IAB11-4': '386', 'IAB1-2': '432', 'IAB5-9': '139', 'IAB6-7': '305', 'IAB5-12': '149', 'IAB5-13': '134', 'IAB19-4': '631',
'IAB19-19': '631', 'IAB19-20': '631', 'IAB19-32': '631', 'IAB9-24': '245', 'IAB21': '441', 'IAB21-3': '451', 'IAB23': '453', 'IAB10-9': '276', 'IAB4-9': '130', 'IAB16-6': '429', 'IAB4-6': '129',
'IAB13-10': '416', 'IAB2-19': '28', 'IAB17-28': '525', 'IAB9-25': '272', 'IAB17-29': '527', 'IAB17-30': '227', 'IAB17-31': '530', 'IAB22-1': '481', 'IAB15': '464', 'IAB9-26': '246', 'IAB9-27': '256',
'IAB9-28': '267', 'IAB17-33': '502', 'IAB19-35': '627', 'IAB2-20': '4', 'IAB7-39': '307', 'IAB19-30': '605', 'IAB22': '473', 'IAB17-34': '503', 'IAB17-35': '531', 'IAB7-19': '309', 'IAB7-40': '310',
'IAB19-6': '635', 'IAB7-41': '237', 'IAB17-36': '504', 'IAB17-44': '533', 'IAB20-23': '662', 'IAB15-7': '472', 'IAB20-24': '671', 'IAB5-14': '136', 'IAB6-8': '199', 'IAB17': '483', 'IAB9-29': '263',
'IAB2-23': '5', 'IAB13-11': '414', 'IAB4-3': '395', 'IAB18': '552', 'IAB7-42': '311', 'IAB17-37': '505', 'IAB17-38': '537', 'IAB17-39': '538', 'IAB19-26': '636', 'IAB19': '596', 'IAB1-7': '640',
'IAB17-40': '539', 'IAB7-43': '293', 'IAB20': '653', 'IAB8-16': '212', 'IAB8-17': '213', 'IAB16-7': '430', 'IAB9-30': '680', 'IAB17-41': '541', 'IAB17-42': '542', 'IAB17-43': '506', 'IAB15-10': '390',
'IAB19-23': '607', 'IAB19-34': '629', 'IAB14-7': '165', 'IAB7-44': '231', 'IAB7-45': '238', 'IAB9-31': '257', 'IAB5-1': '135', 'IAB7-2': '301', 'IAB18-6': '580', 'IAB7-3': '297', 'IAB23-1': '453',
'IAB8-1': '214', 'IAB7-6': '312', 'IAB7-7': '300', 'IAB7-8': '301', 'IAB13-1': '410', 'IAB7-9': '301', 'IAB15-9': '465', 'IAB7-10': '313', 'IAB3-4': '602', 'IAB20-8': '660', 'IAB8-3': '214',
'IAB20-10': '660', 'IAB7-11': '314', 'IAB20-11': '660', 'IAB23-4': '459', 'IAB9-8': '270', 'IAB8-4': '214', 'IAB7-12': '306', 'IAB7-13': '313', 'IAB7-14': '287', 'IAB18-5': '575', 'IAB7-15': '315',
'IAB8-7': '214', 'IAB19-11': '616', 'IAB7-16': '289', 'IAB7-18': '301', 'IAB7-20': '290', 'IAB20-13': '659', 'IAB7-21': '313', 'IAB18-3': '579', 'IAB13-3': '52', 'IAB20-15': '659', 'IAB8-11': '214',
'IAB7-22': '318', 'IAB20-16': '659', 'IAB7-23': '313', 'IAB7': '223', 'IAB10-6': '634', 'IAB7-27': '318', 'IAB11-1': '388', 'IAB7-29': '318', 'IAB7-30': '304', 'IAB19-18': '619', 'IAB8-13': '214',
'IAB20-19': '659', 'IAB20-20': '657', 'IAB8-14': '214', 'IAB18-4': '565', 'IAB23-9': '459', 'IAB11': '379', 'IAB8-15': '214', 'IAB20-21': '662', 'IAB17-21': '492', 'IAB17-22': '518', 'IAB12': '379',
'IAB23-10': '453', 'IAB7-34': '301', 'IAB4-8': '395', 'IAB26-3': '608', 'IAB20-25': '151', 'IAB20-27': '659'
}

export function convertSegment(segment) {
if (!segment) return {}
return {
id: D_IAB_ID[segment.id || segment.ID]
}
}

/**
* map array of objects to segments
* @param {Array[{ID: string}]} normalizable
* @returns array of IAB "segments"
*/
export function pickSegments(normalizable) {
if (Array.isArray(normalizable) === false) return []
return normalizable.map(convertSegment)
.filter(t => t.id)
}

export const neuwoRtdModule = {
name: 'NeuwoRTDModule',
init,
getBidRequestData
}

submodule('realTimeData', neuwoRtdModule)
Loading

0 comments on commit 3195179

Please sign in to comment.