diff --git a/src/auction.ts b/src/auction.ts index 6a7a9643938..d5d6d67032b 100644 --- a/src/auction.ts +++ b/src/auction.ts @@ -29,7 +29,7 @@ import { type Metrics, useMetrics } from './utils/perfMetrics.js'; import { adjustCpm } from './utils/cpm.js'; import { getGlobal } from './prebidGlobal.js'; import { ttlCollection } from './utils/ttlCollection.js'; -import { getMinBidCacheTTL, onMinBidCacheTTLChange } from './bidTTL.js'; +import { getEffectiveMinBidCacheTTL, onMinBidCacheTTLChange } from './bidTTL.js'; import type { Bid, BidResponse } from "./bidfactory.ts"; import type { AdUnitCode, BidderCode, Identifier, ORTBFragments } from './types/common.d.ts'; import type { TargetingMap } from "./targeting.ts"; @@ -197,7 +197,10 @@ export function newAuction({ adUnits, adUnitCodes, callback, cbTimeout, labels, let _bidderRequests: BidderRequest[] = []; const _bidsReceived = ttlCollection({ startTime: (bid) => bid.responseTimestamp, - ttl: (bid) => getMinBidCacheTTL() == null ? null : Math.max(getMinBidCacheTTL(), bid.ttl) * 1000 + ttl: (bid) => { + const minTTL = getEffectiveMinBidCacheTTL(bid); + return minTTL == null ? null : Math.max(minTTL, bid.ttl) * 1000; + } }); let _noBids: BidRequest[] = []; let _winningBids: Bid[] = []; @@ -434,6 +437,7 @@ export function newAuction({ adUnits, adUnitCodes, callback, cbTimeout, labels, function setBidTargeting(bid) { adapterManager.callSetTargetingBidder(bid.adapterCode || bid.bidder, bid); + _bidsReceived.refresh(); } events.on(EVENTS.PBS_ANALYTICS, (event) => { diff --git a/src/auctionManager.js b/src/auctionManager.js index 40395d95f28..9251829497c 100644 --- a/src/auctionManager.js +++ b/src/auctionManager.js @@ -31,7 +31,7 @@ import { AuctionIndex } from './auctionIndex.js'; import { BID_STATUS, JSON_MAPPING } from './constants.js'; import { useMetrics } from './utils/perfMetrics.js'; import { ttlCollection } from './utils/ttlCollection.js'; -import { getMinBidCacheTTL, onMinBidCacheTTLChange } from './bidTTL.js'; +import { getEffectiveMinBidCacheTTL, getMinBidCacheTTL, onMinBidCacheTTLChange } from './bidTTL.js'; /** * Creates new instance of auctionManager. There will only be one instance of auctionManager but @@ -42,8 +42,19 @@ import { getMinBidCacheTTL, onMinBidCacheTTLChange } from './bidTTL.js'; export function newAuctionManager() { const _auctions = ttlCollection({ startTime: (au) => au.end.then(() => au.getAuctionEnd()), - ttl: (au) => getMinBidCacheTTL() == null ? null : au.end.then(() => { - return Math.max(getMinBidCacheTTL(), ...au.getBidsReceived().map(bid => bid.ttl)) * 1000 + ttl: (au) => au.end.then(() => { + const bids = au.getBidsReceived(); + if (bids.length === 0) { + const minTTL = getMinBidCacheTTL(); + return minTTL == null ? null : minTTL * 1000; + } + const ttls = bids.map(bid => { + const minTTL = getEffectiveMinBidCacheTTL(bid); + if (minTTL == null) return null; + return Math.max(minTTL, bid.ttl); + }); + if (ttls.some(t => t == null)) return null; + return Math.max(...ttls) * 1000; }), }); @@ -131,7 +142,10 @@ export function newAuctionManager() { if (bid && status === BID_STATUS.BID_TARGETING_SET) { const auction = getAuction(bid.auctionId); - if (auction) auction.setBidTargeting(bid); + if (auction) { + auction.setBidTargeting(bid); + _auctions.refresh(); + } } } diff --git a/src/bidTTL.ts b/src/bidTTL.ts index 2af2986311c..e56f123a033 100644 --- a/src/bidTTL.ts +++ b/src/bidTTL.ts @@ -1,8 +1,11 @@ import { config } from './config.js'; import { logError } from './utils.js'; +import { BID_STATUS } from './constants.js'; const CACHE_TTL_SETTING = 'minBidCacheTTL'; +const MIN_TARGETED_BID_CACHE_TTL_SETTING = 'minTargetedBidCacheTTL'; let TTL_BUFFER = 1; let minCacheTTL = null; +let minTargetedBidCacheTTL = null; const listeners = []; declare module './config' { @@ -21,6 +24,12 @@ declare module './config' { * If unset (the default), bids are kept for the lifetime of the page. */ [CACHE_TTL_SETTING]?: number; + /** + * When set, overrides minBidCacheTTL for bids that have had targeting set (e.g. bids sent to the ad server). + * Useful with GPT lazy load when the scroll milestone for render may take a long time. + * If unset, minBidCacheTTL applies to all bids. Setting to Infinity keeps targeted bids indefinitely. + */ + [MIN_TARGETED_BID_CACHE_TTL_SETTING]?: number; } } @@ -40,14 +49,47 @@ export function getMinBidCacheTTL() { return minCacheTTL; } +export function getMinTargetedBidCacheTTL() { + return minTargetedBidCacheTTL; +} + +/** + * Returns the effective minimum cache TTL in seconds for a bid. + * When minTargetedBidCacheTTL is set and the bid has had targeting set, uses that; + * otherwise uses minBidCacheTTL. Returns null if no minimum applies (bid kept for page lifetime). + */ +export function getEffectiveMinBidCacheTTL(bid) { + const baseTTL = minCacheTTL; + if (baseTTL == null && minTargetedBidCacheTTL == null) { + return null; + } + if (bid?.status === BID_STATUS.BID_TARGETING_SET && typeof minTargetedBidCacheTTL === 'number') { + return minTargetedBidCacheTTL; + } + return baseTTL; +} + +function notifyCacheTTLChange() { + listeners.forEach(l => l(minCacheTTL)); +} + config.getConfig(CACHE_TTL_SETTING, (cfg) => { const prev = minCacheTTL; minCacheTTL = cfg?.[CACHE_TTL_SETTING]; minCacheTTL = typeof minCacheTTL === 'number' ? minCacheTTL : null; if (prev !== minCacheTTL) { - listeners.forEach(l => l(minCacheTTL)) + notifyCacheTTLChange(); } -}) +}); + +config.getConfig(MIN_TARGETED_BID_CACHE_TTL_SETTING, (cfg) => { + const prev = minTargetedBidCacheTTL; + minTargetedBidCacheTTL = cfg?.[MIN_TARGETED_BID_CACHE_TTL_SETTING]; + minTargetedBidCacheTTL = typeof minTargetedBidCacheTTL === 'number' ? minTargetedBidCacheTTL : null; + if (prev !== minTargetedBidCacheTTL) { + notifyCacheTTLChange(); + } +}); export function onMinBidCacheTTLChange(listener) { listeners.push(listener); diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index 58151526325..935486d52c0 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -7,7 +7,7 @@ import { getPriceByGranularity, addBidResponse, resetAuctionState, responsesReady, newAuction } from 'src/auction.js'; -import { EVENTS, TARGETING_KEYS, S2S } from 'src/constants.js'; +import { BID_STATUS, EVENTS, TARGETING_KEYS, S2S } from 'src/constants.js'; import * as auctionModule from 'src/auction.js'; import { registerBidder } from 'src/adapters/bidderFactory.js'; import { createBid } from 'src/bidfactory.js'; @@ -28,7 +28,7 @@ import { setConfig as setCurrencyConfig } from '../../modules/currency.js' import { REJECTION_REASON } from '../../src/constants.js'; import { setDocumentHidden } from './unit/utils/focusTimeout_spec.js'; import { sandbox } from 'sinon'; -import { getMinBidCacheTTL, onMinBidCacheTTLChange } from '../../src/bidTTL.js'; +import { getEffectiveMinBidCacheTTL, getMinBidCacheTTL, getMinTargetedBidCacheTTL, onMinBidCacheTTLChange } from '../../src/bidTTL.js'; import { getGlobal } from '../../src/prebidGlobal.js'; /** @@ -870,6 +870,29 @@ describe('auctionmanager.js', function () { }) }) + describe('setConfig(minTargetedBidCacheTTL)', () => { + it('should update getMinTargetedBidCacheTTL', () => { + expect(getMinTargetedBidCacheTTL()).to.eql(null); + config.setConfig({ minTargetedBidCacheTTL: 3600 }); + expect(getMinTargetedBidCacheTTL()).to.eql(3600); + }); + + it('getEffectiveMinBidCacheTTL uses minTargetedBidCacheTTL for bids with targeting set', () => { + config.setConfig({ minBidCacheTTL: 30, minTargetedBidCacheTTL: 3600 }); + const bidWithTargeting = { status: BID_STATUS.BID_TARGETING_SET }; + const bidWithoutTargeting = { status: 'other' }; + expect(getEffectiveMinBidCacheTTL(bidWithTargeting)).to.eql(3600); + expect(getEffectiveMinBidCacheTTL(bidWithoutTargeting)).to.eql(30); + }); + + it('getEffectiveMinBidCacheTTL uses minBidCacheTTL when minTargetedBidCacheTTL not set', () => { + config.resetConfig(); + config.setConfig({ minBidCacheTTL: 30 }); + const bidWithTargeting = { status: BID_STATUS.BID_TARGETING_SET }; + expect(getEffectiveMinBidCacheTTL(bidWithTargeting)).to.eql(30); + }) + }) + describe('minBidCacheTTL', () => { let clock, auction; beforeEach(() => { @@ -910,7 +933,7 @@ describe('auctionmanager.js', function () { it('pick up updates to minBidCacheTTL that happen during bid lifetime', async () => { auction.callBids(); - await auction.edn; + await auction.end; clock.tick(10 * 1000); config.setConfig({ minBidCacheTTL: 20 @@ -918,6 +941,37 @@ describe('auctionmanager.js', function () { await clock.tick(0); await clock.tick(20 * 1000); expect(auctionManager.getBidsReceived().length).to.equal(1); + }); + + it('do not expire targeted bids when minTargetedBidCacheTTL is set', async () => { + config.setConfig({ + minBidCacheTTL: 30, + minTargetedBidCacheTTL: 3600 + }); + bids = [ + { + adUnitCode: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, + ttl: 10, + adId: utils.getUniqueIdentifierStr(), + auctionId: auction.getAuctionId() + }, { + adUnitCode: ADUNIT_CODE, + adUnitId: ADUNIT_CODE, + ttl: 100, + adId: utils.getUniqueIdentifierStr(), + auctionId: auction.getAuctionId() + } + ]; + auction.callBids(); + await auction.end; + const shortTtlBid = auctionManager.getBidsReceived().find(b => b.ttl === 10); + auctionManager.setStatusForBids(shortTtlBid.adId, BID_STATUS.BID_TARGETING_SET); + await clock.tick(105 * 1000); + await clock.tick(0); + const bidsReceived = auctionManager.getBidsReceived(); + expect(bidsReceived.length).to.equal(1); + expect(bidsReceived[0].adId).to.equal(shortTtlBid.adId); }) })