Skip to content

Commit

Permalink
priceFloors: fix bug where default does not work on adUnit-level fl…
Browse files Browse the repository at this point in the history
…oors
  • Loading branch information
dgirardi committed Sep 12, 2023
1 parent 4c2902f commit 9720c7c
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 17 deletions.
39 changes: 35 additions & 4 deletions modules/priceFloors.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@ const MODULE_NAME = 'Price Floors';
*/
const ajax = ajaxBuilder(10000);

// eslint-disable-next-line symbol-description
const SYN_FIELD = Symbol();

/**
* @summary Allowed fields for rules to have
*/
export let allowedFields = ['gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType'];
export let allowedFields = [SYN_FIELD, 'gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType'];

/**
* @summary This is a flag to indicate if a AJAX call is processing for a floors request
Expand Down Expand Up @@ -104,6 +107,7 @@ function getAdUnitCode(request, response, {index = auctionManager.index} = {}) {
* @summary floor field types with their matching functions to resolve the actual matched value
*/
export let fieldMatchingFunctions = {
[SYN_FIELD]: () => '*',
'size': (bidRequest, bidResponse) => parseGPTSingleSizeArray(bidResponse.size) || '*',
'mediaType': (bidRequest, bidResponse) => bidResponse.mediaType || 'banner',
'gptSlot': (bidRequest, bidResponse) => getGptSlotFromAdUnit((bidRequest || bidResponse).transactionId) || getGptSlotInfoForAdUnitCode(getAdUnitCode(bidRequest, bidResponse)).gptSlot,
Expand All @@ -117,6 +121,7 @@ export let fieldMatchingFunctions = {
* Returns array of Tuple [exact match, catch all] for each field in rules file
*/
function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) {
if (!floorFields.length) return [];
// generate combination of all exact matches and catch all for each field type
return floorFields.reduce((accum, field) => {
let exactMatch = fieldMatchingFunctions[field](bidObject, responseObject) || '*';
Expand All @@ -132,7 +137,9 @@ function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) {
*/
export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) {
let fieldValues = enumeratePossibleFieldValues(deepAccess(floorData, 'schema.fields') || [], bidObject, responseObject);
if (!fieldValues.length) return { matchingFloor: floorData.default };
if (!fieldValues.length) {
return {matchingFloor: undefined}
}

// look to see if a request for this context was made already
let matchingInput = fieldValues.map(field => field[0]).join('-');
Expand All @@ -146,9 +153,9 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {})

let matchingData = {
floorMin: floorData.floorMin || 0,
floorRuleValue: isNaN(floorData.values[matchingRule]) ? floorData.default : floorData.values[matchingRule],
floorRuleValue: floorData.values[matchingRule],
matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters
matchingRule
matchingRule: matchingRule === floorData.meta?.defaultRule ? undefined : matchingRule
};
// use adUnit floorMin as priority!
const floorMin = deepAccess(bidObject, 'ortb2Imp.ext.prebid.floors.floorMin');
Expand Down Expand Up @@ -443,7 +450,31 @@ function validateRules(floorsData, numFields, delimiter) {
return Object.keys(floorsData.values).length > 0;
}

export function normalizeDefault(model) {
if (isNumber(model.default)) {
const numFields = (model.schema?.fields || []).length;
if (!numFields) {
deepSetValue(model, 'schema.fields', [SYN_FIELD]);
Object.assign(model, {
values: {'*': model.default},
meta: {
defaultRule: '*'
}
})
} else {
const defaultRule = Array(numFields).fill('*').join(model.schema?.delimiter || '|');
model.values = model.values || {};
if (model.values[defaultRule] == null) {
model.values[defaultRule] = model.default;
model.meta = {defaultRule};
}
}
}
return model;
}

function modelIsValid(model) {
model = normalizeDefault(model);
// schema.fields has only allowed attributes
if (!validateSchemaFields(deepAccess(model, 'schema.fields'))) {
return false;
Expand Down
109 changes: 96 additions & 13 deletions test/spec/modules/priceFloors_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
isFloorsDataValid,
addBidResponseHook,
fieldMatchingFunctions,
allowedFields
allowedFields, parseFloorData, normalizeDefault
} from 'modules/priceFloors.js';
import * as events from 'src/events.js';
import * as mockGpt from '../integration/faker/googletag.js';
Expand Down Expand Up @@ -123,7 +123,7 @@ describe('the price floors module', function () {
return {
code,
mediaTypes: {banner: { sizes: [[300, 200], [300, 600]] }, native: {}},
bids: [{bidder: 'someBidder'}, {bidder: 'someOtherBidder'}]
bids: [{bidder: 'someBidder', adUnitCode: code}, {bidder: 'someOtherBidder', adUnitCode: code}]
};
}
beforeEach(function() {
Expand All @@ -143,6 +143,15 @@ describe('the price floors module', function () {
getGlobal().bidderSettings = {};
});

describe('parseFloorData', () => {
it('should accept just a default floor', () => {
const fd = parseFloorData({
default: 1.23
});
expect(getFirstMatchingFloor(fd, {}, {}).matchingFloor).to.eql(1.23);
});
});

describe('getFloorsDataForAuction', function () {
it('converts basic input floor data into a floorData map for the auction correctly', function () {
// basic input where nothing needs to be updated
Expand Down Expand Up @@ -233,8 +242,8 @@ describe('the price floors module', function () {
});

describe('getFirstMatchingFloor', function () {
it('uses a 0 floor as overrite', function () {
let inputFloorData = {
it('uses a 0 floor as override', function () {
let inputFloorData = normalizeDefault({
currency: 'USD',
schema: {
delimiter: '|',
Expand All @@ -245,7 +254,7 @@ describe('the price floors module', function () {
'test_div_2': 2
},
default: 0.5
};
});

expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({
floorMin: 0,
Expand Down Expand Up @@ -434,7 +443,7 @@ describe('the price floors module', function () {
});
});
it('selects the right floor for more complex rules', function () {
let inputFloorData = {
let inputFloorData = normalizeDefault({
currency: 'USD',
schema: {
delimiter: '^',
Expand All @@ -448,7 +457,7 @@ describe('the price floors module', function () {
'weird_div^*^300x250': 5.5
},
default: 0.5
};
});
// banner with 300x250 size
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({
floorMin: 0,
Expand Down Expand Up @@ -490,10 +499,8 @@ describe('the price floors module', function () {
matchingFloor: undefined
});
// if default is there use it
inputFloorData = { default: 5.0 };
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({
matchingFloor: 5.0
});
inputFloorData = normalizeDefault({ default: 5.0 });
expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'}).matchingFloor).to.equal(5.0);
});
describe('with gpt enabled', function () {
let gptFloorData;
Expand Down Expand Up @@ -693,6 +700,82 @@ describe('the price floors module', function () {
floorProvider: undefined
});
});
describe('default floor', () => {
let adUnits;
beforeEach(() => {
adUnits = ['au1', 'au2'].map(getAdUnitMock);
})
function expectFloors(floors) {
runStandardAuction(adUnits);
adUnits.forEach((au, i) => {
au.bids.forEach(bid => {
expect(bid.getFloor().floor).to.eql(floors[i]);
})
})
}
describe('should be sufficient by itself', () => {
it('globally', () => {
handleSetFloorsConfig({
...basicFloorConfig,
data: {
default: 1.23
}
});
expectFloors([1.23, 1.23])
});
it('on adUnits', () => {
handleSetFloorsConfig({
...basicFloorConfig,
data: undefined
});
adUnits[0].floors = {default: 1};
adUnits[1].floors = {default: 2};
expectFloors([1, 2])
})
});
describe('should NOT be used when a star rule exists', () => {
it('globally', () => {
handleSetFloorsConfig({
...basicFloorConfig,
data: {
schema: {
fields: ['mediaType', 'gptSlot'],
},
values: {
'*|*': 2
},
default: 3,
}
});
expectFloors([2, 2]);
});
it('on adUnits', () => {
handleSetFloorsConfig({
...basicFloorConfig,
data: undefined
});
adUnits[0].floors = {
schema: {
fields: ['mediaType'],
},
values: {
'*': 1
},
default: 3
};
adUnits[1].floors = {
schema: {
fields: ['gptSlot'],
},
values: {
'*': 2
},
default: 4
}
expectFloors([1, 2]);
})
});
})
it('bidRequests should have getFloor function and flooring meta data when setConfig occurs', function () {
handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider'});
runStandardAuction();
Expand Down Expand Up @@ -1382,7 +1465,7 @@ describe('the price floors module', function () {
it('picks the right rule with more complex rules', function () {
_floorDataForAuction[bidRequest.auctionId] = {
...basicFloorConfig,
data: {
data: parseFloorData({
currency: 'USD',
schema: { fields: ['mediaType', 'size'], delimiter: '|' },
values: {
Expand All @@ -1394,7 +1477,7 @@ describe('the price floors module', function () {
'video|*': 5.5
},
default: 10.0
}
})
};

// assumes banner *
Expand Down

0 comments on commit 9720c7c

Please sign in to comment.