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

Permutive RTD submodule #6290

Merged
merged 6 commits into from
Feb 12, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
223 changes: 223 additions & 0 deletions integrationExamples/gpt/permutiveRtdProvider_example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
<html>

<head>
<link rel="icon" type="image/png" href="/favicon.png">
<script async src="//www.googletagservices.com/tag/js/gpt.js"></script>
<script src="../../build/dev/prebid.js" async></script>
<script>
window.permutive = {};
window.permutive.ready = () => {};

function setLocalStorageData () {
const data = {
_pdfps: ['gam1', 'gam2'],
_prubicons: ['rubicon1', 'rubicon2'],
_papns: ['appnexus1', 'appnexus2'],
_psegs: ['1234', '1000001', '1000002'],
_ppam: ['ppam1', 'ppam2'],
_pcrprs: ['pcrprs1', 'pcrprs2']
}

for (let key in data) {
window.localStorage[key] = JSON.stringify(data[key])
}
}

setLocalStorageData()

var div_1_sizes = [
[300, 250],
[300, 600]
];
var div_2_sizes = [
[728, 90],
[970, 250]
];
var PREBID_TIMEOUT = 2500;
var FAILSAFE_TIMEOUT = 3000;

var adUnits = [
{
code: '/19968336/header-bid-tag-0',
mediaTypes: {
banner: {
sizes: div_1_sizes
}
},
bids: [
{
bidder: 'appnexus',
params: {
placementId: 13144370,
keywords: {
inline_kvs: ['1']
}
}
},
{
bidder: 'rubicon',
params: {
accountId: '9840',
siteId: '123564',
zoneId: '583584',
inventory: {
area: ['home']
},
visitor: {
inline_kvs: ['1']
}
}
},
{
bidder: 'ozone',
params: {
publisherId: 'OZONEGMG0001',
siteId: '4204204209',
placementId: '0420420500',
customData: [
{
settings: {},
targeting: {
inline_kvs: ['1', '2', '3', '4']
jdwieland8282 marked this conversation as resolved.
Show resolved Hide resolved
}
}
],
ozoneData: {}
}
}
]
},
{
code: '/19968336/header-bid-tag-1',
mediaTypes: {
banner: {
sizes: div_2_sizes
}
},
bids: [
{
bidder: 'appnexus',
params: {
placementId: 13144370
}
},
{
bidder: 'ozone',
params: {
publisherId: 'OZONEGMG0001',
siteId: '4204204209',
placementId: '0420420500'
}
}
]
}
];


var googletag = googletag || {};
googletag.cmd = googletag.cmd || [];
googletag.cmd.push(function() {
googletag.pubads().disableInitialLoad();
});

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

pbjs.que.push(function() {
pbjs.setConfig({
debug: true,
realTimeData: {
auctionDelay: 800, // maximum time for RTD modules to respond
bretg marked this conversation as resolved.
Show resolved Hide resolved
dataProviders: [
{
name: 'permutive',
waitForIt: true,
params: {
acBidders: ['appnexus', 'rubicon', 'ozone'],
maxSegs: 500,
overwrites: {
rubicon: function (bid, data, acEnabled, utils, defaultFn) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW - while this is ok for Feb 10, it hoped/expected that most adapters will soon be able to start reading ortb2.user.data to get segments. When rubicon supports this, will want to update the example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout - please see comment below

if (defaultFn){
bid = defaultFn(bid, data, acEnabled)
}
if (data.gam && data.gam.length) {
utils.deepSetValue(bid, 'params.visitor.permutive', data.gam)
}
}
}
}
}
]
}
});
pbjs.addAdUnits(adUnits);
requestBids();
});

function requestBids () {
pbjs.que.push(function() {
pbjs.requestBids({
bidsBackHandler: initAdserver,
timeout: PREBID_TIMEOUT
});
});
}

function initAdserver() {
if (pbjs.initAdserverSet) return;
pbjs.initAdserverSet = true;
googletag.cmd.push(function() {
pbjs.que.push(function() {
pbjs.setTargetingForGPTAsync();
googletag.pubads().refresh();
});
});
}
// in case PBJS doesn't load
setTimeout(function() {
initAdserver();
}, FAILSAFE_TIMEOUT);

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>
<p><button onclick="requestBids()">Refresh Ad Unit</button></p>
<h2>Basic Prebid.js Example</h2>
<h5>Div-1</h5>
<div id="div-1">
<script type="text/javascript">
googletag.cmd.push(function() {
googletag.display("div-1");
});

</script>
</div>

<br>

<h5>Div-2</h5>
<div id="div-2">
<script type="text/javascript">
googletag.cmd.push(function() {
googletag.display("div-2");
});

</script>
</div>

</body>

</html>
171 changes: 171 additions & 0 deletions modules/permutiveRtdProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* This module adds permutive provider to the real time data module
* The {@link module:modules/realTimeData} module is required
* The module will add custom targeting to ad units of specific partners
* @module modules/permutiveRtdProvider
* @requires module:modules/realTimeData
*/
import { getGlobal } from '../src/prebidGlobal.js'
import { submodule } from '../src/hook.js'
import { getStorageManager } from '../src/storageManager.js'
import { deepSetValue, deepAccess, isFn, mergeDeep } from '../src/utils.js'
import includes from 'core-js-pure/features/array/includes.js'

export const storage = getStorageManager()

function init (config, userConsent) {
return true
}

/**
* Set targeting from cache and then try to wait for Permutive
* to initialise to get realtime targeting
*/
export function initTargeting (reqBidsConfigObj, callback, customConfig) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename this function to something like "initSegments". Prebid RTD modules uses the word "targeting" to mean ad server targeting -- as a reviewer, I find it confusing to sort out the terminology.

const permutiveOnPage = isPermutiveOnPage()
const config = mergeDeep({
waitForIt: false,
params: {
maxSegs: 500,
acBidders: [],
overwrites: {}
}
}, customConfig)

setTargeting(reqBidsConfigObj, config)

if (config.waitForIt && permutiveOnPage) {
window.permutive.ready(function () {
setTargeting(reqBidsConfigObj, config)
callback()
}, 'realtime')
} else {
callback()
}
}

function setTargeting (reqBidsConfigObj, config) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also consider conforming to the new segment syntax defined by the Prebid Taxonomy committee and the IAB. ee #6057

I understand that right now this is a draft spec not implemented by the adapters, but... Chicken and Egg. If you start writing the segments now, the bidders can start picking them up when they're ready.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally agree. We went for parity on how segments are passed into bidders as custom params today (today our clients add custom code to their sites, that sets bidder config as params). But we'd also prefer to use the proper FPD object instead. The issues we came across right now is that bidders do not accept and parse the new structure yet.

In the example below data isn't pared as expected:

pbjs.setConfig({
  fpd: {
    user: {
      data: [
        {
          "name": "www.dataprovider1.com",
          "ext": { "taxonomyname": "Example Proprietary Taxonomy ABC" },
          "segment": [
            { "id": "687" },
            { "id": "123" }
          ]
        },
        {
          "name": "www.dataprovider1.com",
          "ext": { "taxonomyname": "IAB Audience Taxonomy" },
          "segment": [
            { "id": "687" },
            { "id": "123" }
          ]
        }
      ]
    }
  }
})

results in:
image

Based on you other comment, I assume the idea is that we'd use fpd.user.ortb2.data instead of fpd.user.data? So set segments like below:

pbjs.setConfig({
  fpd: {
    user: {
      ortb2: {
        data: [
          {
            "name": "www.dataprovider1.com",
            "ext": { "taxonomyname": "Example Proprietary Taxonomy ABC" },
            "segment": [
              { "id": "687" },
              { "id": "123" }
            ]
          },
          {
            "name": "www.dataprovider1.com",
            "ext": { "taxonomyname": "IAB Audience Taxonomy" },
            "segment": [
              { "id": "687" },
              { "id": "123" }
            ]
          }
        ]
      }
    }
  }
})

What we could do is keep the current structure to make sure targeting works in the here and now, but then also set fpd.user.ortb2.data in parallel for when adapters accept that structure. Is this what you recommend? There are still a few open questions on our end to decide how to expose the right name and taxonomyname and what these values should be for different use-cases. If you're happy with it, I'd suggest to go ahead with v1 which takes the same approach as the current setup and we'll then follow up with a PR for v2 which adds support for fpd.user.ortb2.data - ready for when individual adapters start picking it up.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the adapters aren't as of yet accepting user.data.segment. But I know that conversation is happening right now in the Magnite(rubicon) exchange team, so it won't be long before we can update the rubicon adapter to pass in the segments where they want them.

So for now, I'm suggesting pass the data BOTH ways: as bidder.params.visitor and as ortb2.user.data. Bidders will ignore the latter for now, but it being there from the start kickstarts the chicken/egg problem.

For the record, 'fpd' setConfig will be gone once #6293 is merged. So I recommend ignoring 'fpd' starting now -- I recommend doing a "getConfig" on ortb2, adding your stuff to the user.data array, and setConfiging it back.

pbjs.setConfig({
    ortb2: {
      ... stuff that publisher may have already added ...
      user: {
        data: [ { ... } ]
     }
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes perfect sense, thanks @bretg. Is it ok if we follow up with a separate PR for this? Implementing the change should be quick on our end. But we'll need to first understand what the appropriate values for name and taxonomyname are for the different use-cases we'll support with this module. Great to hear that the Magnite team is already working on supporting user.data.segment!

Also, thanks for your note on getConfig before calling setConfiging - we want to make sure we're not overwriting any other segment data objects

const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits
const data = getSegments(config.params.maxSegs)
const utils = { deepSetValue, deepAccess, isFn, mergeDeep }

adUnits.forEach(adUnit => {
adUnit.bids.forEach(bid => {
const { bidder } = bid
const acEnabled = isAcEnabled(config, bidder)
const customFn = getCustomBidderFn(config, bidder)
const defaultFn = getDefaultBidderFn(bidder)

if (customFn) {
customFn(bid, data, acEnabled, utils, defaultFn)
} else if (defaultFn) {
defaultFn(bid, data, acEnabled)
} else {

}
})
})
}

function getCustomBidderFn (config, bidder) {
const overwriteFn = deepAccess(config, `params.overwrites.${bidder}`)

if (overwriteFn && isFn(overwriteFn)) {
return overwriteFn
} else {
return null
}
}

/**
* Returns a function that receives a `bid` object, a `data` object and a `acEnabled` boolean
* and which will set the right targeting keys for `bid` based on `data` and `acEnabled`
* @param {string} bidder
* @param {object} data
*/
function getDefaultBidderFn (bidder) {
const bidderMapper = {
appnexus: function (bid, data, acEnabled) {
if (acEnabled && data.ac && data.ac.length) {
deepSetValue(bid, 'params.keywords.p_standard', data.ac)
}
if (data.appnexus && data.appnexus.length) {
deepSetValue(bid, 'params.keywords.permutive', data.appnexus)
}

return bid
},
rubicon: function (bid, data, acEnabled) {
if (acEnabled && data.ac && data.ac.length) {
deepSetValue(bid, 'params.visitor.p_standard', data.ac)
}
if (data.rubicon && data.rubicon.length) {
deepSetValue(bid, 'params.visitor.permutive', data.rubicon)
}

return bid
},
ozone: function (bid, data, acEnabled) {
if (acEnabled && data.ac && data.ac.length) {
deepSetValue(bid, 'params.customData.0.targeting.p_standard', data.ac)
}

return bid
}
}

return bidderMapper[bidder]
}

export function isAcEnabled (config, bidder) {
const acBidders = deepAccess(config, 'params.acBidders') || []
return includes(acBidders, bidder)
}

export function isPermutiveOnPage () {
return typeof window.permutive !== 'undefined' && typeof window.permutive.ready === 'function'
}

/**
* Returns all relevant segment IDs in an object
*/
export function getSegments (maxSegs) {
const legacySegs = readSegments('_psegs').map(Number).filter(seg => seg >= 1000000).map(String)
const _ppam = readSegments('_ppam')
const _pcrprs = readSegments('_pcrprs')

const segments = {
ac: [..._pcrprs, ..._ppam, ...legacySegs],
rubicon: readSegments('_prubicons'),
appnexus: readSegments('_papns'),
gam: readSegments('_pdfps')
}

for (const type in segments) {
segments[type] = segments[type].slice(0, maxSegs)
}

return segments
}

/**
* Gets an array of segment IDs from LocalStorage
* or returns an empty array
* @param {string} key
*/
function readSegments (key) {
try {
return JSON.parse(storage.getDataFromLocalStorage(key) || '[]')
} catch (e) {
return []
}
}

/** @type {RtdSubmodule} */
export const permutiveSubmodule = {
name: 'permutive',
getBidRequestData: initTargeting,
init: init
}

submodule('realTimeData', permutiveSubmodule)
Loading