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

Currency Module: Adding auction delay handling #12364

Merged
merged 6 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
39 changes: 37 additions & 2 deletions modules/currency.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import {config} from '../src/config.js';
import {getHook} from '../src/hook.js';
import {defer} from '../src/utils/promise.js';
import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js';
import {timedBidResponseHook} from '../src/utils/perfMetrics.js';
import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.js';
import {on as onEvent, off as offEvent} from '../src/events.js';
import {continueAuction as continueAuctionNative, resumeDelayedAuctions} from '../src/auction.js';

const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$';
const CURRENCY_RATE_PRECISION = 4;
const MODULE_NAME = 'currency';

let ratesURL;
let bidResponseQueue = [];
let conversionCache = {};
let currencyRatesLoaded = false;
export let currencyRatesLoaded = false;
let needToCallForCurrencyFile = true;
let adServerCurrency = 'USD';

Expand All @@ -26,6 +28,9 @@ let defaultRates;

export let responseReady = defer();

export let delayedAuctions = [];
let auctionDelay = 0;

/**
* Configuration function for currency
* @param {object} config
Expand Down Expand Up @@ -77,6 +82,7 @@ export function setConfig(config) {
}

if (typeof config.adServerCurrency === 'string') {
auctionDelay = config.auctionDelay;
logInfo('enabling currency support', arguments);

adServerCurrency = config.adServerCurrency;
Expand Down Expand Up @@ -106,6 +112,7 @@ export function setConfig(config) {
initCurrency();
} else {
// currency support is disabled, setting defaults
auctionDelay = 0;
logInfo('disabling currency support');
resetCurrency();
}
Expand Down Expand Up @@ -137,6 +144,7 @@ function loadRates() {
conversionCache = {};
currencyRatesLoaded = true;
processBidResponseQueue();
resumeDelayedAuctions(delayedAuctions, continueAuction);
} catch (e) {
errorSettingsRates('Failed to parse currencyRates response: ' + response);
}
Expand All @@ -145,6 +153,7 @@ function loadRates() {
errorSettingsRates(...args);
currencyRatesLoaded = true;
processBidResponseQueue();
resumeDelayedAuctions(delayedAuctions, continueAuction);
needToCallForCurrencyFile = true;
}
}
Expand All @@ -162,6 +171,7 @@ function initCurrency() {
getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency);
getHook('addBidResponse').before(addBidResponseHook, 100);
getHook('responsesReady').before(responsesReadyHook);
getHook('requestBids').before(requestBidsHook, 50);
onEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout);
onEvent(EVENTS.AUCTION_INIT, loadRates);
loadRates();
Expand All @@ -172,6 +182,7 @@ function resetCurrency() {
if (currencySupportEnabled) {
getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove();
getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove();
getHook('requestBids').getHooks({hook: requestBidsHook}).remove();
offEvent(EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout);
offEvent(EVENTS.AUCTION_INIT, loadRates);
delete getGlobal().convertCurrency;
Expand Down Expand Up @@ -335,3 +346,27 @@ export function setOrtbCurrency(ortbRequest, bidderRequest, context) {
}

registerOrtbProcessor({type: REQUEST, name: 'currency', fn: setOrtbCurrency});

export const requestBidsHook = timedAuctionHook('currency', function requestBidsHook(fn, reqBidsConfigObj) {
const hookConfig = {
reqBidsConfigObj,
context: this,
nextFn: fn,
haveExited: false,
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is not used anywhere. Probably a typo and it was meant to be hasExited (which would also be falsy if left undefined).

timer: null
};

if (!currencyRatesLoaded && auctionDelay > 0) {
hookConfig.timer = setTimeout(() => {
logWarn(`${MODULE_NAME}: Fetch attempt did not return in time for auction ${reqBidsConfigObj.auctionId}`)
continueAuction(hookConfig);
}, auctionDelay);
delayedAuctions.push(hookConfig);
} else {
continueAuction(hookConfig);
}
});

function continueAuction(config) {
continueAuctionNative(config, delayedAuctions);
}
33 changes: 5 additions & 28 deletions modules/priceFloors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
deepAccess,
deepClone,
deepSetValue,
generateUUID,
getParameterByName,
isNumber,
logError,
Expand All @@ -30,6 +29,7 @@ import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.j
import {adjustCpm} from '../src/utils/cpm.js';
import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js';
import {convertCurrency} from '../libraries/currencyUtils/currency.js';
import {continueAuction as continueAuctionNative, resumeDelayedAuctions} from '../src/auction.js';

export const FLOOR_SKIPPED_REASON = {
NOT_FOUND: 'not_found',
Expand Down Expand Up @@ -436,20 +436,10 @@ export function createFloorsDataForAuction(adUnits, auctionId) {
* @summary This is the function which will be called to exit our module and continue the auction.
*/
export function continueAuction(hookConfig) {
// only run if hasExited
if (!hookConfig.hasExited) {
// if this current auction is still fetching, remove it from the _delayedAuctions
_delayedAuctions = _delayedAuctions.filter(auctionConfig => auctionConfig.timer !== hookConfig.timer);

// We need to know the auctionId at this time. So we will use the passed in one or generate and set it ourselves
hookConfig.reqBidsConfigObj.auctionId = hookConfig.reqBidsConfigObj.auctionId || generateUUID();

continueAuctionNative(hookConfig, _delayedAuctions, () => {
// now we do what we need to with adUnits and save the data object to be used for getFloor and enforcement calls
_floorDataForAuction[hookConfig.reqBidsConfigObj.auctionId] = createFloorsDataForAuction(hookConfig.reqBidsConfigObj.adUnits || getGlobal().adUnits, hookConfig.reqBidsConfigObj.auctionId);

hookConfig.nextFn.apply(hookConfig.context, [hookConfig.reqBidsConfigObj]);
hookConfig.hasExited = true;
}
})
}

function validateSchemaFields(fields) {
Expand Down Expand Up @@ -594,19 +584,6 @@ export const requestBidsHook = timedAuctionHook('priceFloors', function requestB
}
});

/**
* @summary If an auction was queued to be delayed (waiting for a fetch) then this function will resume
* those delayed auctions when delay is hit or success return or fail return
*/
function resumeDelayedAuctions() {
_delayedAuctions.forEach(auctionConfig => {
// clear the timeout
clearTimeout(auctionConfig.timer);
continueAuction(auctionConfig);
});
_delayedAuctions = [];
}

/**
* This function handles the ajax response which comes from the user set URL to fetch floors data from
* @param {object} fetchResponse The floors data response which came back from the url configured in config.floors
Expand All @@ -631,7 +608,7 @@ export function handleFetchResponse(fetchResponse) {
}

// if any auctions are waiting for fetch to finish, we need to continue them!
resumeDelayedAuctions();
resumeDelayedAuctions(_delayedAuctions, continueAuction);
}

function handleFetchError(status) {
Expand All @@ -640,7 +617,7 @@ function handleFetchError(status) {
logError(`${MODULE_NAME}: Fetch errored with: `, status);

// if any auctions are waiting for fetch to finish, we need to continue them!
resumeDelayedAuctions();
resumeDelayedAuctions(_delayedAuctions, continueAuction);
}

/**
Expand Down
35 changes: 35 additions & 0 deletions src/auction.js
Original file line number Diff line number Diff line change
Expand Up @@ -933,3 +933,38 @@ function isValidPrice(bid) {
if (!maxBidValue || !bid.cpm) return true;
return maxBidValue >= Number(bid.cpm);
}

/**
* @summary This is the function which will be called to exit module and continue the auction.
*/
export function continueAuction(hookConfig, delayedAuctions, callback) {
patmmccann marked this conversation as resolved.
Show resolved Hide resolved
// only run if hasExited
if (!hookConfig.hasExited) {
// if this current auction is still fetching, remove it from the _delayedAuctions
const auctionIndex = delayedAuctions.findIndex(auctionConfig => auctionConfig.timer === hookConfig.timer);
delayedAuctions.splice(auctionIndex, 1);

// We need to know the auctionId at this time. So we will use the passed in one or generate and set it ourselves
hookConfig.reqBidsConfigObj.auctionId = hookConfig.reqBidsConfigObj.auctionId || generateUUID();
patmmccann marked this conversation as resolved.
Show resolved Hide resolved

if (callback) {
callback();
}

hookConfig.nextFn.apply(hookConfig.context, [hookConfig.reqBidsConfigObj]);
hookConfig.hasExited = true;
}
}

/**
* @summary If an auction was queued to be delayed (waiting for a fetch) then this function will resume
* those delayed auctions when delay is hit or success return or fail return
*/
export function resumeDelayedAuctions(delayedAuctions, continueAuctionFn) {
delayedAuctions.forEach(auctionConfig => {
// clear the timeout
clearTimeout(auctionConfig.timer);
continueAuctionFn(auctionConfig);
});
delayedAuctions.length = 0;
}
57 changes: 57 additions & 0 deletions test/spec/modules/currency_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import {
responseReady
} from 'modules/currency.js';
import {createBid} from '../../../src/bidfactory.js';
import * as utils from 'src/utils.js';
import { EVENTS, STATUS, REJECTION_REASON } from '../../../src/constants.js';
import {server} from '../../mocks/xhr.js';
import * as events from 'src/events.js';
import { delayedAuctions, requestBidsHook } from '../../../modules/currency.js';

var assert = require('chai').assert;
var expect = require('chai').expect;
Expand Down Expand Up @@ -522,4 +524,59 @@ describe('currency', function () {
expect(innerBid.currency).to.equal('CNY');
});
});

describe('auctionDelay param', () => {
const continueAuction = sinon.stub();
let logWarnSpy;

beforeEach(function() {
sandbox = sinon.sandbox.create();
clock = sinon.useFakeTimers(1046952000000); // 2003-03-06T12:00:00Z
logWarnSpy = sinon.spy(utils, 'logWarn');
});

afterEach(function () {
sandbox.restore();
clock.restore();
delayedAuctions.length = 0;
utils.logWarn.restore();
continueAuction.resetHistory();
});

it('should delay auction start when auctionDelay set in module config', () => {
setConfig({auctionDelay: 2000, adServerCurrency: 'USD'});
const reqBidsConfigObj = {
auctionId: '128937'
};
requestBidsHook(continueAuction, reqBidsConfigObj);
clock.tick(1000);
expect(delayedAuctions.length).to.deep.equal(1);
expect(continueAuction.notCalled).to.be.true;
expect(delayedAuctions[0].timer).to.not.be.undefined;
});

it('should start auction when auctionDelay time passed', () => {
setConfig({auctionDelay: 2000, adServerCurrency: 'USD'});
const reqBidsConfigObj = {
auctionId: '128937'
};
requestBidsHook(continueAuction, reqBidsConfigObj);
expect(delayedAuctions.length).to.deep.equal(1);
clock.tick(3000);
expect(delayedAuctions.length).to.deep.equal(0);
expect(logWarnSpy.calledOnce).to.equal(true);
expect(continueAuction.calledOnce).to.be.true;
});

it('should run auction if rates were fetched before auctionDelay time', () => {
setConfig({auctionDelay: 3000, adServerCurrency: 'USD'});
const reqBidsConfigObj = {
auctionId: '128937'
};
fakeCurrencyFileServer.respond();
requestBidsHook(continueAuction, reqBidsConfigObj);
expect(continueAuction.calledOnce).to.be.true;
expect(delayedAuctions.length).to.deep.equal(0);
});
});
});