Skip to content

Commit

Permalink
Set prebid.bidfloor and prebid.bidfloorcur in fledge auction signals
Browse files Browse the repository at this point in the history
  • Loading branch information
dgirardi committed Aug 24, 2023
1 parent 785c119 commit 5c3f371
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 53 deletions.
48 changes: 36 additions & 12 deletions modules/fledgeForGpt.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
*/
import { config } from '../src/config.js';
import { getHook } from '../src/hook.js';
import { getGptSlotForAdUnitCode, logInfo, logWarn } from '../src/utils.js';
import {deepSetValue, getGptSlotForAdUnitCode, logInfo, logWarn, mergeDeep} from '../src/utils.js';
import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js';
import * as events from '../src/events.js'
import CONSTANTS from '../src/constants.json';
import {currencyCompare} from '../libraries/currencyUtils/currency.js';
import {maximum, minimum} from '../src/utils/reducers.js';

const MODULE = 'fledgeForGpt'
const PENDING = {};
Expand Down Expand Up @@ -41,30 +43,52 @@ export function init(cfg) {
}
}

function setComponentAuction(adUnitCode, gptSlot, componentAuctionConfig) {
const seller = componentAuctionConfig.seller;
function setComponentAuction(adUnitCode, auctionConfigs) {
const gptSlot = getGptSlotForAdUnitCode(adUnitCode);
if (gptSlot && gptSlot.setConfig) {
gptSlot.setConfig({
componentAuction: [{
configKey: seller,
auctionConfig: componentAuctionConfig
}]
componentAuction: auctionConfigs.map(cfg => ({
configKey: cfg.seller,
auctionConfig: cfg
}))
});
logInfo(MODULE, `register component auction config for: ${adUnitCode} x ${seller}: ${gptSlot.getAdUnitPath()}`, componentAuctionConfig);
logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs);
} else {
logWarn(MODULE, `unable to register component auction config for: ${adUnitCode} x ${seller}.`);
logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs);
}
}

function onAuctionInit({auctionId}) {
PENDING[auctionId] = {};
}

function getSlotSignals(bidsReceived = [], bidRequests = []) {
let bidfloor, bidfloorcur;
if (bidsReceived.length > 0) {
const bestBid = bidsReceived.reduce(maximum(currencyCompare(bid => [bid.cpm, bid.currency])));
bidfloor = bestBid.cpm;
bidfloorcur = bestBid.currency;
} else {
const floors = bidRequests.map(bid => typeof bid.getFloor === 'function' && bid.getFloor()).filter(f => f);
const minFloor = floors.length && floors.reduce(minimum(currencyCompare(floor => [floor.floor, floor.currency])))
bidfloor = minFloor?.floor;
bidfloorcur = minFloor?.currency;
}
const cfg = {};
if (bidfloor) {
deepSetValue(cfg, 'auctionSignals.prebid.bidfloor', bidfloor);
bidfloorcur && deepSetValue(cfg, 'auctionSignals.prebid.bidfloorcur', bidfloorcur);
}
return cfg;
}

function onAuctionEnd({auctionId, bidsReceived, bidderRequests}) {
try {
const allReqs = bidderRequests?.flatMap(br => br.bids);
Object.entries(PENDING[auctionId]).forEach(([adUnitCode, auctionConfigs]) => {
const gptSlot = getGptSlotForAdUnitCode(adUnitCode);
auctionConfigs.forEach(cfg => setComponentAuction(adUnitCode, gptSlot, cfg));
const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode;
const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit));
setComponentAuction(adUnitCode, auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg)))
})
} finally {
delete PENDING[auctionId];
Expand All @@ -76,7 +100,7 @@ export function addComponentAuctionHook(next, auctionId, adUnitCode, componentAu
!PENDING[auctionId].hasOwnProperty(adUnitCode) && (PENDING[auctionId][adUnitCode] = []);
PENDING[auctionId][adUnitCode].push(componentAuctionConfig);
} else {
logWarn(MODULE, `Received component auction config for auction that is already over (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig)
logWarn(MODULE, `Received component auction config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig)
}
next(auctionId, adUnitCode, componentAuctionConfig);
}
Expand Down
3 changes: 0 additions & 3 deletions modules/gdprEnforcement.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,6 @@ export const reportAnalyticsRule = gdprRule(7, () => purpose7Rule, analyticsBloc

export const ufpdRule = gdprRule(4, () => purpose4Rule, ufpdBlocked);

export const defaultEidRule = function (params) {

}
/**
* Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event.
*/
Expand Down
2 changes: 1 addition & 1 deletion modules/prebidServerBidAdapter/ortbConverter.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ const PBS_CONVERTER = ortbConverter({
const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur]));
let min;
for (const req of context.actualBidRequests.values()) {
let floor = {};
const floor = {};
orig(floor, req, context);
// if any bid does not have a valid floor, do not attempt to send any to PBS
if (floor.bidfloorcur == null || floor.bidfloor == null) {
Expand Down
163 changes: 137 additions & 26 deletions test/spec/modules/fledgeForGpt_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,39 @@ import 'modules/rubiconBidAdapter.js';
import {parseExtPrebidFledge, setImpExtAe, setResponseFledgeConfigs} from 'modules/fledgeForGpt.js';
import * as events from 'src/events.js';
import CONSTANTS from 'src/constants.json';
import {getGlobal} from '../../../src/prebidGlobal.js';

describe('fledgeForGpt module', () => {
let sandbox;

beforeEach(() => {
sandbox = sinon.sandbox.create();
})
});
afterEach(() => {
sandbox.restore();
})
describe('addComponentAuction', function() {
});
describe('addComponentAuction', function () {
before(() => {
fledge.init({enabled: true})
fledge.init({enabled: true});
});

const fledgeAuctionConfig = {
seller: 'bidder',
mock: 'config'
}
};

describe('addComponentAuctionHook', function() {
describe('addComponentAuctionHook', function () {
let nextFnSpy, mockGptSlot;
beforeEach(function() {
beforeEach(function () {
nextFnSpy = sinon.spy();
mockGptSlot = {
setConfig: sinon.stub(),
getAdUnitPath: () => 'mock/gpt/au'
}
sandbox.stub(utils, 'getGptSlotForAdUnitCode').callsFake(() => mockGptSlot)
};
sandbox.stub(utils, 'getGptSlotForAdUnitCode').callsFake(() => mockGptSlot);
});

it('should call next()', function() {
it('should call next()', function () {
fledge.addComponentAuctionHook(nextFnSpy, 'aid', 'auc', fledgeAuctionConfig);
sinon.assert.calledWith(nextFnSpy, 'aid', 'auc', fledgeAuctionConfig);
});
Expand All @@ -52,8 +53,8 @@ describe('fledgeForGpt module', () => {
const cf1 = {...fledgeAuctionConfig, id: 1, seller: 'b1'};
const cf2 = {...fledgeAuctionConfig, id: 2, seller: 'b2'};
fledge.addComponentAuctionHook(nextFnSpy, 'aid', 'au1', cf1);
fledge.addComponentAuctionHook(nextFnSpy, 'aid', 'au2', cf2)
events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'})
fledge.addComponentAuctionHook(nextFnSpy, 'aid', 'au2', cf2);
events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'});
sinon.assert.calledWith(utils.getGptSlotForAdUnitCode, 'au1');
sinon.assert.calledWith(utils.getGptSlotForAdUnitCode, 'au2');
sinon.assert.calledWith(mockGptSlot.setConfig, {
Expand All @@ -67,32 +68,139 @@ describe('fledgeForGpt module', () => {
configKey: 'b2',
auctionConfig: cf2,
}]
})
})
});
});

it('should drop auction configs after end of auction', () => {
events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'});
events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'});
fledge.addComponentAuctionHook(nextFnSpy, 'aid', 'au', fledgeAuctionConfig);
events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'});
sinon.assert.notCalled(mockGptSlot.setConfig);
})
});

describe('floor signal', () => {
before(() => {
if (!getGlobal().convertCurrency) {
getGlobal().convertCurrency = () => null;
getGlobal().convertCurrency.mock = true;
}
});
after(() => {
if (getGlobal().convertCurrency.mock) {
delete getGlobal().convertCurrency;
}
});

beforeEach(() => {
sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amount, from, to) => {
if (from === to) return amount;
if (from === 'USD' && to === 'JPY') return amount * 100;
if (from === 'JPY' && to === 'USD') return amount / 100;
throw new Error('unexpected currency conversion');
});
});

Object.entries({
'bids': (payload, values) => {
payload.bidsReceived = values
.map((val) => ({adUnitCode: 'au', cpm: val.amount, currency: val.cur}))
.concat([{adUnitCode: 'other', cpm: 10000, currency: 'EUR'}])
},
'no bids': (payload, values) => {
payload.bidderRequests = values
.map((val) => ({bids: [{adUnitCode: 'au', getFloor: () => ({floor: val.amount, currency: val.cur})}]}))
.concat([{bids: {adUnitCode: 'other', getFloor: () => ({floor: -10000, currency: 'EUR'})}}])
}
}).forEach(([tcase, setup]) => {
describe(`when auction has ${tcase}`, () => {
Object.entries({
'no currencies': {
values: [{amount: 1}, {amount: 100}, {amount: 10}, {amount: 100}],
'bids': {
bidfloor: 100,
bidfloorcur: undefined
},
'no bids': {
bidfloor: 1,
bidfloorcur: undefined,
}
},
'only zero values': {
values: [{amount: 0, cur: 'USD'}, {amount: 0, cur: 'JPY'}],
'bids': {
bidfloor: undefined,
bidfloorcur: undefined,
},
'no bids': {
bidfloor: undefined,
bidfloorcur: undefined,
}
},
'matching currencies': {
values: [{amount: 10, cur: 'JPY'}, {amount: 100, cur: 'JPY'}],
'bids': {
bidfloor: 100,
bidfloorcur: 'JPY',
},
'no bids': {
bidfloor: 10,
bidfloorcur: 'JPY',
}
},
'mixed currencies': {
values: [{amount: 10, cur: 'USD'}, {amount: 10, cur: 'JPY'}],
'bids': {
bidfloor: 10,
bidfloorcur: 'USD'
},
'no bids': {
bidfloor: 10,
bidfloorcur: 'JPY',
}
}
}).forEach(([t, testConfig]) => {
const values = testConfig.values;
const {bidfloor, bidfloorcur} = testConfig[tcase];

describe(`with ${t}`, () => {
let payload;
beforeEach(() => {
payload = {auctionId: 'aid'};
setup(payload, values);
});

it('should populate bidfloor/bidfloorcur', () => {
events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'});
fledge.addComponentAuctionHook(nextFnSpy, 'aid', 'au', fledgeAuctionConfig);
events.emit(CONSTANTS.EVENTS.AUCTION_END, payload);
sinon.assert.calledWith(mockGptSlot.setConfig, sinon.match(arg => {
return arg.componentAuction.some(au => au.auctionConfig.auctionSignals?.prebid?.bidfloor === bidfloor && au.auctionConfig.auctionSignals?.prebid?.bidfloorcur === bidfloorcur)
}))
})
});
});
})
})
});
});
});

describe('fledgeEnabled', function () {
const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]]))
const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]]));

before(function () {
// navigator.runAdAuction & co may not exist, so we can't stub it normally with
// sinon.stub(navigator, 'runAdAuction') or something
Object.keys(navProps).forEach(p => { navigator[p] = sinon.stub() });
Object.keys(navProps).forEach(p => {
navigator[p] = sinon.stub();
});
hook.ready();
});

after(function() {
after(function () {
Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig);
})
});

afterEach(function () {
config.resetConfig();
Expand All @@ -117,7 +225,7 @@ describe('fledgeForGpt module', () => {

describe('with setBidderConfig()', () => {
it('should set fledgeEnabled correctly per bidder', function () {
config.setConfig({bidderSequence: 'fixed'})
config.setConfig({bidderSequence: 'fixed'});
config.setBidderConfig({
bidders: ['appnexus'],
config: {
Expand All @@ -130,7 +238,8 @@ describe('fledgeForGpt module', () => {
adUnits,
Date.now(),
utils.getUniqueIdentifierStr(),
function callback() {},
function callback() {
},
[]
);

Expand Down Expand Up @@ -159,7 +268,8 @@ describe('fledgeForGpt module', () => {
adUnits,
Date.now(),
utils.getUniqueIdentifierStr(),
function callback() {},
function callback() {
},
[]
);

Expand All @@ -185,7 +295,8 @@ describe('fledgeForGpt module', () => {
adUnits,
Date.now(),
utils.getUniqueIdentifierStr(),
function callback() {},
function callback() {
},
[]
);

Expand Down Expand Up @@ -218,7 +329,7 @@ describe('fledgeForGpt module', () => {
const imp = {ext: {ae: 1}};
setImpExtAe(imp, {}, {bidderRequest: {}});
expect(imp.ext.ae).to.not.exist;
})
});
it('imp.ext.ae should be left intact if fledge is enabled', () => {
const imp = {ext: {ae: 2}};
setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}});
Expand All @@ -235,7 +346,7 @@ describe('fledgeForGpt module', () => {
}
}
}
}
};
}

function generateImpCtx(fledgeFlags) {
Expand Down Expand Up @@ -315,4 +426,4 @@ describe('fledgeForGpt module', () => {
});
});
});
})
});
14 changes: 7 additions & 7 deletions test/spec/modules/multibid_spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {expect} from 'chai';
import {
addBidResponseHook,
adjustBidderRequestsHook,
resetMultibidUnits,
resetMultiConfig,
sortByMultibid,
targetBidPoolHook,
validateMultibid
addBidResponseHook,
adjustBidderRequestsHook,
resetMultibidUnits,
resetMultiConfig,
sortByMultibid,
targetBidPoolHook,
validateMultibid
} from 'modules/multibid/index.js';
import {config} from 'src/config.js';
import {getHighestCpm} from '../../../src/utils/reducers.js';
Expand Down
Loading

0 comments on commit 5c3f371

Please sign in to comment.