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

PAAPI: add top level auction example #11259

Merged
merged 10 commits into from
Apr 17, 2024
10 changes: 9 additions & 1 deletion gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,15 @@ function startLocalServer(options = {}) {
port: port,
host: INTEG_SERVER_HOST,
root: './',
livereload: options.livereload
livereload: options.livereload,
middleware: function () {
return [
function (req, res, next) {
res.setHeader('Ad-Auction-Allowed', 'True');
next();
}
];
}
});
}

Expand Down
57 changes: 57 additions & 0 deletions integrationExamples/gpt/top-level-paapi/decisionLogic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
function logPrefix(scope) {
return [
`%c PAAPI %c ${scope} %c`,
'color: green; background-color:yellow; border: 1px solid black',
'color: blue; border:1px solid black',
'',
];
}

function scoreAd(
adMetadata,
bid,
auctionConfig,
trustedScoringSignals,
browserSignals,
directFromSellerSignals
) {
console.group(...logPrefix('scoreAd'), 'Buyer:', browserSignals.interestGroupOwner);
console.log('Context:', JSON.stringify({
adMetadata,
bid,
auctionConfig: {
...auctionConfig,
componentAuctions: '[omitted]'
},
trustedScoringSignals,
browserSignals,
directFromSellerSignals
}, ' ', ' '));

const result = {
desirability: bid,
allowComponentAuction: true,
};
const {bidfloor, bidfloorcur} = auctionConfig.auctionSignals?.prebid || {};
if (bidfloor) {
if (browserSignals.bidCurrency !== '???' && browserSignals.bidCurrency !== bidfloorcur) {
console.log(`Floor currency (${bidfloorcur}) does not match bid currency (${browserSignals.bidCurrency}), and currency conversion is not yet implemented. Rejecting bid.`);
result.desirability = -1;
} else if (bid < bidfloor) {
console.log(`Bid (${bid}) lower than contextual winner/floor (${bidfloor}). Rejecting bid.`);
result.desirability = -1;
result.rejectReason = 'bid-below-auction-floor';
}
}
console.log('Result:', result);
console.groupEnd();
return result;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

reportResult should still be defined even if it does not do anything, to avoid

Worklet error: https://127.0.0.1:9999/integrationExamples/gpt/top-level-paapi/decisionLogic.js reportResult is not a function.

Could be something like this

function reportResult(auctionConfig, browserSignals) {
  log('reportResult', { auctionConfig, browserSignals });
  sendReportTo(`${auctionConfig.seller}/report/win?${Object.entries(browserSignals).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')}`);
  return {}
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

that would replace it with an error about the report call failing :) I am already worried about people copying this demo into production so I'd rather avoid it; the plan is to work with @patmmccann to publish an actual reference implementation separately

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this about the event url showing up as an error in the Network tab ? It's still useful to look at parameters, but if unwanted can comment out sendReportTo or make it conditional on something like sellerSignals.reportURL


function reportResult(auctionConfig, browserSignals) {
console.group(...logPrefix('reportResult'));
console.log('Context', JSON.stringify({auctionConfig, browserSignals}, ' ', ' '));
console.groupEnd();
sendReportTo(`${auctionConfig.seller}/report/win?${Object.entries(browserSignals).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')}`);
return {};
}
188 changes: 188 additions & 0 deletions integrationExamples/gpt/top-level-paapi/tl_paapi_example.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<html>
<head>
<script async src="../../../../build/dev/prebid.js"></script>
<script>
// intercept navigator.runAdAuction and print parameters to console
(() => {
var originalRunAdAuction = navigator.runAdAuction;
navigator.runAdAuction = function (...args) {
console.log('%c runAdAuction', 'background: cyan; border: 2px; border-radius: 3px', ...args);
return originalRunAdAuction.apply(navigator, args);
};
})();
</script>

<script async src="https://securepubads.g.doubleclick.net/tag/js/gpt.js"></script>

<script>
var FAILSAFE_TIMEOUT = 3300;
var PREBID_TIMEOUT = 3000;
var adUnits = [{
code: 'div-1',
mediaTypes: {
banner: {
sizes: [[300, 250]],
}
},
ortb2Imp: {
ext: {
ae: 1
}
},
bids: [
{
bidder: 'openx',
params: {
unit: '538703464',
response_template_name: 'test_banner_ad',
test: true,
delDomain: 'sademo-d.openx.net'
}
},

],
}
]
;

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

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

pbjs.que.push(function () {
pbjs.setConfig({
debug: true,
paapi: {
enabled: true,
gpt: {
autoconfig: false
}
},
debugging: {
enabled: true,
intercept: [
{
when: {
bidder: 'openx',
},
then: {
cpm: 0.1
},
paapi() {
return [
{
'seller': 'https://privacysandbox.openx.net',
'decisionLogicURL': 'https://privacysandbox.openx.net/fledge/decision-logic-component.js',
'sellerSignals': {
'floor': 0.01,
'currency': 'USD',
'auctionTimestamp': new Date().getTime(),
'publisherId': '537143056',
'adUnitId': '538703464'
},
'interestGroupBuyers': [
'https://privacysandbox.openx.net'
],
'perBuyerSignals': {
'https://privacysandbox.openx.net': {
'bid': 1.5
}
},
'sellerCurrency': 'USD'
}
];
}
}
]
},
});

pbjs.addAdUnits(adUnits);
pbjs.requestBids({
bidsBackHandler: sendAdserverRequest,
timeout: PREBID_TIMEOUT
});
});

function raa() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

does it make sense to have some of this code in a submodule, which might end up being extremely compact on initial commit, but would provide the convenient logical abstraction that a user would pick the PAAPI submodule associated with their choice of TLS?

Copy link
Member

@jdwieland8282 jdwieland8282 Mar 27, 2024

Choose a reason for hiding this comment

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

I think so yes. A pub makes 2 decisions.

  1. I want to run PAAPI with prebid
  2. I want to do it myself (pub sub module) or I want GPT to do it for me

return Promise.all(
Object.entries(pbjs.getPAAPIConfig())
.map(([adUnitCode, auctionConfig]) => {
return navigator.runAdAuction({
seller: window.location.origin,
decisionLogicURL: new URL('decisionLogic.js', window.location).toString(),
resolveToConfig: false,
...auctionConfig
}).then(urn => {
if (urn) {
// if we have a paapi winner, replace the adunit div
// with an iframe that renders it
const iframe = document.createElement('iframe');
Object.assign(iframe, {
src: urn,
frameBorder: 0,
scrolling: 'no',
}, auctionConfig.requestedSize);
const div = document.getElementById(adUnitCode);
div.parentElement.insertBefore(iframe, div);
div.remove();
return true;
}
});
})
).then(won => won.every(el => el));
}

function sendAdserverRequest() {
if (pbjs.adserverRequestSent) return;
pbjs.adserverRequestSent = true;
raa().then((allPaapi) => {
if (!allPaapi) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this calls gam if paapi doesn't fill, correct? But PAAPI will always fill if we give it the best contextual bid from prebid as its best alternative? Is the below code basically saying 'Let's compete Prebid vs PAAPI, and if neither provide any ad call gam?'

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think we can give it the best contextual bid. Some bidders may decide to provide "extra" bids for it, but those don't necessarily contain all, or even the same, contextual bids, and Prebid can't control them.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's auctionSignals.prebid.bidfloor no ? I don't know if all buyers take it into account, so it may still not fill even if everyone bid all the time and above the floor.

Anyway, I assume that deleting the <div> is what makes GAM skip that slot if it was filled by PAAPI. Is there a better way to disable the slot more explicitly and perhaps less permanent ? It's fine either way as a test page but I imagine it won't work if I wanted to periodically refresh the ads.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, this is not a great way to render. I am not sure yet what the render API will look like:

  • when using GAM, the use case I'm hearing is to run fledge only after GAM picks the Prebid contextual winner (to keep adx demand in GAM). In practice this would mean returning the URN back to the rendering logic running in the GAM creative (PUC or equivalent). The publisher would not call any rendering API but somehow enable this behavior.
  • when not using GAM, the publisher calls renderAd(document, adId). We could do something similar and only "pretend" to render adId, rendering the fledge ad instead in a nested frame, if the publisher enabled the same behavior as above. Or we could provide separate runPaapi/renderPaapi APIs.
  • I'm not sure there will be a use case for what's demoed here, only run gam if fledge has no winner. Pubs would probably prefer to let GAM run fledge and get better demand.

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

setTimeout(function () {
sendAdserverRequest();
}, FAILSAFE_TIMEOUT);

googletag.cmd.push(function () {
googletag.defineSlot('/19968336/header-bid-tag-0', [[300, 250], [300, 600]], 'div-1').addService(googletag.pubads());

googletag.pubads().enableSingleRequest();
googletag.enableServices();
});
</script>
</head>

<body>
<h2>Standalone PAAPI Prebid.js Example</h2>
<p>Start local server with:</p>
<code>gulp serve-fast --https</code>
<p>Chrome flags:</p>
<code>--enable-features=CookieDeprecationFacilitatedTesting:label/treatment_1.2/force_eligible/true
--privacy-sandbox-enrollment-overrides=https://localhost:9999</code>
<p>Join interest group at <a href="https://privacysandbox.openx.net/fledge/advertiser">https://privacysandbox.openx.net/fledge/advertiser</a>
</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

Can make it fully local with

navigator.joinAdInterestGroup({
  name: 'prebid-test',
  owner: window.location.origin,
  biddingLogicURL: (new URL('buyer.js', window.location)).toString(),
  ads: [{
    renderURL: (new URL('ad.html', window.location)).toString(),
  }],
  adComponents: []
}, 600);

and a short buyer js

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

My intention was to make this as real-world as possible but I couldn't get any adapter that would both work and agree to be in here. Mocking openx' response was a compromise, hopefully temporary.

Copy link
Contributor

Choose a reason for hiding this comment

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

I was not sure if the demo needed to be self-contained. If so, the buyer can be local too, at a small cost of being slightly confusing to share a domain with seller. It's fine either way.

Mocking the openx adapter response is also fine - I was not aware this is possible so thanks for showing me how to do it :)

<h5>Div-1</h5>
<div id='div-1' style='min-width: 300px; min-height: 250px;'>
<script type='text/javascript'>
googletag.cmd.push(function () {
googletag.display('div-1');
});
</script>
</div>

</body>
</html>
2 changes: 1 addition & 1 deletion libraries/creative-renderer-native/renderer.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 18 additions & 4 deletions modules/paapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
import {config} from '../src/config.js';
import {getHook, module} from '../src/hook.js';
import {deepSetValue, logInfo, logWarn, mergeDeep} from '../src/utils.js';
import {deepSetValue, logInfo, logWarn, mergeDeep, parseSizesInput} from '../src/utils.js';
import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js';
import * as events from '../src/events.js';
import {EVENTS} from '../src/constants.js';
Expand Down Expand Up @@ -89,19 +89,33 @@ function getSlotSignals(bidsReceived = [], bidRequests = []) {
return cfg;
}

function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes}) {
function onAuctionEnd({auctionId, bidsReceived, bidderRequests, adUnitCodes, adUnits}) {
const adUnitsByCode = Object.fromEntries(adUnits?.map(au => [au.code, au]) || [])
const allReqs = bidderRequests?.flatMap(br => br.bids);
const paapiConfigs = {};
(adUnitCodes || []).forEach(au => {
paapiConfigs[au] = null;
!latestAuctionForAdUnit.hasOwnProperty(au) && (latestAuctionForAdUnit[au] = null);
})
});
Object.entries(pendingForAuction(auctionId) || {}).forEach(([adUnitCode, auctionConfigs]) => {
const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode;
const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit));
paapiConfigs[adUnitCode] = {
...slotSignals,
componentAuctions: auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg))
};
// TODO: need to flesh out size treatment:
// - which size should the paapi auction pick? (this uses the first one defined)
// - should we signal it to SSPs, and how?
// - what should we do if adapters pick a different one?
// - what does size mean for video and native?
const size = parseSizesInput(adUnitsByCode[adUnitCode]?.mediaTypes?.banner?.sizes)?.[0]?.split('x');
if (size) {
paapiConfigs[adUnitCode].requestedSize = {
width: size[0],
height: size[1],
};
}
latestAuctionForAdUnit[adUnitCode] = auctionId;
});
configsForAuction(auctionId, paapiConfigs);
Expand Down Expand Up @@ -159,7 +173,7 @@ export function getPAAPIConfig({auctionId, adUnitCode} = {}, includeBlanks = fal
output[au] = null;
}
}
})
});
return output;
}

Expand Down
32 changes: 29 additions & 3 deletions test/spec/modules/paapi_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,30 @@ describe('paapi module', () => {
expect(getPAAPIConfig({auctionId})).to.eql({});
});

it('should use first size as requestedSize', () => {
addComponentAuctionHook(nextFnSpy, {
auctionId,
adUnitCode: 'au1',
}, fledgeAuctionConfig);
events.emit(EVENTS.AUCTION_END, {
auctionId,
adUnits: [
{
code: 'au1',
mediaTypes: {
banner: {
sizes: [[200, 100], [300, 200]]
}
}
}
]
});
expect(getPAAPIConfig({auctionId}).au1.requestedSize).to.eql({
width: '200',
height: '100'
})
})

it('should augment auctionSignals with FPD', () => {
addComponentAuctionHook(nextFnSpy, {
auctionId,
Expand Down Expand Up @@ -295,9 +319,11 @@ describe('paapi module', () => {
it('should populate bidfloor/bidfloorcur', () => {
addComponentAuctionHook(nextFnSpy, {auctionId, adUnitCode: 'au'}, fledgeAuctionConfig);
events.emit(EVENTS.AUCTION_END, payload);
const signals = getPAAPIConfig({auctionId}).au.componentAuctions[0].auctionSignals;
expect(signals.prebid?.bidfloor).to.eql(bidfloor);
expect(signals.prebid?.bidfloorcur).to.eql(bidfloorcur);
const cfg = getPAAPIConfig({auctionId}).au;
const signals = cfg.auctionSignals;
sinon.assert.match(cfg.componentAuctions[0].auctionSignals, signals || {});
expect(signals?.prebid?.bidfloor).to.eql(bidfloor);
expect(signals?.prebid?.bidfloorcur).to.eql(bidfloorcur);
});
});
});
Expand Down