Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions docs/application-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ There are two ways to configure application settings: database and file. This do
- `auction.truncate-target-attr` - Maximum targeting attributes size. Values between 1 and 255.
- `auction.default-integration` - Default integration to assume.
- `auction.debug-allow` - enables debug output in the auction response. Default `true`.
- `auction.impression-limit` - a max number of impressions allowed for the auction, impressions that exceed this limit will be dropped, 0 means no limit.
- `auction.bid-validations.banner-creative-max-size` - Overrides creative max size validation for banners. Valid values
are:
- "skip": don't do anything about creative max size for this publisher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,10 @@ private Future<BidRequest> updateBidRequest(AuctionContext auctionContext) {
.map(bidRequest -> overrideParameters(bidRequest, httpRequest, auctionContext.getPrebidErrors()))
.map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, true))
.map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings()))
.compose(resolvedBidRequest -> ortb2RequestFactory.limitImpressions(
account,
resolvedBidRequest,
auctionContext.getDebugWarnings()))
.compose(resolvedBidRequest -> ortb2RequestFactory.validateRequest(
account,
resolvedBidRequest,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ private Future<BidRequest> updateAndValidateBidRequest(AuctionContext auctionCon

return storedRequestProcessor.processAuctionRequest(account.getId(), auctionContext.getBidRequest())
.compose(auctionStoredResult -> updateBidRequest(auctionStoredResult, auctionContext))
.compose(bidRequest -> ortb2RequestFactory.limitImpressions(account, bidRequest, debugWarnings))
.compose(bidRequest -> ortb2RequestFactory.validateRequest(
account, bidRequest, httpRequest, auctionContext.getDebugContext(), debugWarnings))
.map(interstitialProcessor::process);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.iab.openrtb.request.Dooh;
import com.iab.openrtb.request.Eid;
import com.iab.openrtb.request.Geo;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.request.Publisher;
import com.iab.openrtb.request.Regs;
import com.iab.openrtb.request.Site;
Expand Down Expand Up @@ -192,6 +193,23 @@ public Future<ActivityInfrastructure> activityInfrastructureFrom(AuctionContext
auctionContext.getDebugContext().getTraceLevel()));
}

public Future<BidRequest> limitImpressions(Account account, BidRequest bidRequest, List<String> warnings) {
final List<Imp> imps = bidRequest.getImp();
final int impsLimit = Optional.ofNullable(account)
.map(Account::getAuction)
.map(AccountAuctionConfig::getImpressionLimit)
.orElse(0);

if (impsLimit > 0 && imps.size() > impsLimit) {
metrics.updateImpsDroppedMetric(imps.size() - impsLimit);
warnings.add(("Only first %d impressions were kept due to the limit, "
+ "all the subsequent impressions have been dropped for the auction").formatted(impsLimit));
return Future.succeededFuture(bidRequest.toBuilder().imp(imps.subList(0, impsLimit)).build());
}

return Future.succeededFuture(bidRequest);
}

public Future<BidRequest> validateRequest(Account account,
BidRequest bidRequest,
HttpRequestContext httpRequestContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ public Future<WithPodErrors<AuctionContext>> fromRequest(RoutingContext routingC

.map(auctionContext -> auctionContext.with(debugResolver.debugContextFrom(auctionContext)))

.compose(auctionContext -> ortb2RequestFactory.limitImpressions(
auctionContext.getAccount(),
auctionContext.getBidRequest(),
auctionContext.getDebugWarnings())
.map(auctionContext::with))

.compose(auctionContext -> ortb2RequestFactory.validateRequest(
auctionContext.getAccount(),
auctionContext.getBidRequest(),
Expand Down
1 change: 1 addition & 0 deletions src/main/java/org/prebid/server/metric/MetricName.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public enum MetricName {
request_time,
prices,
imps_requested,
imps_dropped,
imps_banner,
imps_video,
imps_native,
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/prebid/server/metric/Metrics.java
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@ public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean l
incCounter(MetricName.imps_requested, numImps);
}

public void updateImpsDroppedMetric(int numImps) {
incCounter(MetricName.imps_dropped, numImps);
}

public void updateImpTypesMetrics(List<Imp> imps) {

final Map<String, Long> mediaTypeToCount = imps.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,7 @@ public class AccountAuctionConfig {
AccountCacheConfig cache;

AccountBidRankingConfig ranking;

@JsonAlias("impression-limit")
Integer impressionLimit;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class AccountAuctionConfig {
@JsonProperty("bidadjustments")
BidAdjustment bidAdjustments
BidRounding bidRounding
Integer impressionLimit

@JsonProperty("price_granularity")
PriceGranularityType priceGranularitySnakeCase
Expand All @@ -54,4 +55,7 @@ class AccountAuctionConfig {
AccountPriceFloorsConfig priceFloorsSnakeCase
@JsonProperty("bid_rounding")
BidRounding bidRoundingSnakeCase
@JsonProperty("impression_limit")
Integer impressionLimitSnakeCase

}
169 changes: 166 additions & 3 deletions src/test/groovy/org/prebid/server/functional/tests/AuctionSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import org.prebid.server.functional.model.db.Account
import org.prebid.server.functional.model.request.auction.BidRequest
import org.prebid.server.functional.model.request.auction.Device
import org.prebid.server.functional.model.request.auction.DeviceExt
import org.prebid.server.functional.model.request.auction.Imp
import org.prebid.server.functional.model.request.auction.PrebidStoredRequest
import org.prebid.server.functional.model.request.auction.Renderer
import org.prebid.server.functional.model.request.auction.RendererData
Expand Down Expand Up @@ -47,15 +48,18 @@ class AuctionSpec extends BaseSpec {
private static final Integer DEFAULT_TIMEOUT = getRandomTimeout()
private static final Integer MIN_BID_ID_LENGTH = 17
private static final Integer DEFAULT_UUID_LENGTH = 36
private static final Map<String, String> PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String,
"auction.default-timeout-ms": DEFAULT_TIMEOUT as String]
private static final Map<String, String> GENERIC_CONFIG = [
"adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url" : USER_SYNC_URL,
"adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()]

@Shared
PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG)

private static final String IMPS_REQUESTED_METRIC = 'imps_requested'
private static final String IMPS_DROPPED_METRIC = 'imps_dropped'
private static final Integer IMP_LIMIT = 1
private static final Map<String, String> PBS_CONFIG = ["auction.biddertmax.max" : MAX_TIMEOUT as String,
"auction.default-timeout-ms": DEFAULT_TIMEOUT as String]

def "PBS should return version in response header for auction request for #description"() {
when: "PBS processes auction request"
def response = defaultPbsService.sendAuctionRequestRaw(bidRequest)
Expand Down Expand Up @@ -721,4 +725,163 @@ class AuctionSpec extends BaseSpec {
cleanup: "Stop and remove pbs container"
pbsServiceFactory.removeContainer(pbsConfig)
}

def "PBS should drop extra impressions with warnings when number of impressions exceeds impression-limit"() {
given: "Bid request with multiple imps"
def bidRequest = BidRequest.defaultBidRequest.tap {
imp.add(Imp.getDefaultImpression())
}

and: "Account in the DB with impression limit config"
def accountConfig = new AccountConfig(auction: accountAuctionConfig)
def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
accountDao.save(account)

and: "Flush metrics"
flushMetrics(defaultPbsService)

when: "PBS processes auction request"
def response = defaultPbsService.sendAuctionRequest(bidRequest)

then: "Response should contain seatNonBid"
assert !response?.ext?.seatnonbid

and: "PBS should emit an warning"
assert response.ext?.warnings[PREBID]*.code == [999]
assert response.ext?.warnings[PREBID]*.message ==
["Only first $IMP_LIMIT impressions were kept due to the limit, " +
"all the subsequent impressions have been dropped for the auction" as String]

and: "PBS shouldn't emit an error"
assert !response.ext?.errors

and: "Metrics for imps should be updated"
def metrics = defaultPbsService.sendCollectedMetricsRequest()
assert metrics[IMPS_DROPPED_METRIC] == bidRequest.imp.size() - IMP_LIMIT
assert metrics[IMPS_REQUESTED_METRIC] == IMP_LIMIT

and: "Response should contain seat bid"
assert response.seatbid[0].bid.size() == IMP_LIMIT

and: "Bidder request should contain imps according to limit"
assert bidder.getBidderRequest(bidRequest.id).imp.size() == IMP_LIMIT

where:
accountAuctionConfig << [
new AccountAuctionConfig(impressionLimit: IMP_LIMIT),
new AccountAuctionConfig(impressionLimitSnakeCase: IMP_LIMIT)
]
}

def "PBS shouldn't drop extra impressions when number of impressions equal to impression-limit"() {
given: "Bid request with multiple imps"
def bidRequest = BidRequest.defaultBidRequest.tap {
imp.add(Imp.getDefaultImpression())
}

and: "Account in the DB with impression limit config"
def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: bidRequest.imp.size()))
def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
accountDao.save(account)

and: "Flush metrics"
flushMetrics(defaultPbsService)

when: "PBS processes auction request"
def response = defaultPbsService.sendAuctionRequest(bidRequest)

then: "Response should contain seatNonBid"
assert !response?.ext?.seatnonbid

and: "Response shouldn't contain warnings and error"
assert !response.ext?.warnings
assert !response.ext?.errors

and: "Metrics for imps requested should be updated"
def metrics = defaultPbsService.sendCollectedMetricsRequest()
assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size()
assert !metrics[IMPS_DROPPED_METRIC]

and: "Response should contain seat bid"
assert response.seatbid[0].bid.size() == bidRequest.imp.size()

and: "Bidder request should contain originals imps"
assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size()
}

def "PBS shouldn't drop extra impressions when number of impressions less than or equal to impression-limit"() {
given: "Bid request with multiple imps"
def bidRequest = BidRequest.defaultBidRequest.tap {
imp.add(Imp.getDefaultImpression())
}

and: "Account in the DB with impression limit config"
def impressionLimit = bidRequest.imp.size() + 1
def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: impressionLimit))
def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
accountDao.save(account)

and: "Flush metrics"
flushMetrics(defaultPbsService)

when: "PBS processes auction request"
def response = defaultPbsService.sendAuctionRequest(bidRequest)

then: "Response should contain seatNonBid"
assert !response?.ext?.seatnonbid

and: "Response shouldn't contain warnings and error"
assert !response.ext?.warnings
assert !response.ext?.errors

and: "Metrics for imps requested should be updated"
def metrics = defaultPbsService.sendCollectedMetricsRequest()
assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size()
assert !metrics[IMPS_DROPPED_METRIC]

and: "Response should contain seat bid"
assert response.seatbid[0].bid.size() == bidRequest.imp.size()

and: "Bidder request should contain originals imps"
assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size()
}

def "PBS shouldn't drop extra impressions when impression-limit set to #impressionLimit"() {
given: "Bid request with multiple imps"
def bidRequest = BidRequest.defaultBidRequest.tap {
imp.add(Imp.getDefaultImpression())
}

and: "Account in the DB with impression limit config"
def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(impressionLimit: impressionLimit))
def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
accountDao.save(account)

and: "Flush metrics"
flushMetrics(defaultPbsService)

when: "PBS processes auction request"
def response = defaultPbsService.sendAuctionRequest(bidRequest)

then: "Response should contain seatNonBid"
assert !response?.ext?.seatnonbid

and: "Response shouldn't contain warnings and error"
assert !response.ext?.warnings
assert !response.ext?.errors

and: "Metrics for imps requested should be updated"
def metrics = defaultPbsService.sendCollectedMetricsRequest()
assert metrics[IMPS_REQUESTED_METRIC] == bidRequest.imp.size()
assert !metrics[IMPS_DROPPED_METRIC]

and: "Response should contain seat bid"
assert response.seatbid[0].bid.size() == bidRequest.imp.size()

and: "Bidder request should contain originals imps"
assert bidder.getBidderRequest(bidRequest.id).imp.size() == bidRequest.imp.size()

where:
impressionLimit << [null, PBSUtils.randomNegativeNumber, 0]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,8 @@ private void givenBidRequest(

given(ortb2ImplicitParametersResolver.resolve(any(), any(), any(), anyBoolean())).willAnswer(
answerWithFirstArgument());
given(ortb2RequestFactory.limitImpressions(any(), any(), any()))
.willAnswer(invocation -> Future.succeededFuture((BidRequest) invocation.getArgument(1)));
given(ortb2RequestFactory.validateRequest(any(), any(), any(), any(), any()))
.willAnswer(invocation -> Future.succeededFuture((BidRequest) invocation.getArgument(1)));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ public void setUp() {
given(ortb2RequestFactory.executeRawAuctionRequestHooks(any()))
.willAnswer(invocation -> Future.succeededFuture(
((AuctionContext) invocation.getArgument(0)).getBidRequest()));
given(ortb2RequestFactory.limitImpressions(any(), any(), any()))
.willAnswer(invocationOnMock -> Future.succeededFuture(invocationOnMock.getArgument(1)));
given(ortb2RequestFactory.validateRequest(any(), any(), any(), any(), any()))
.willAnswer(invocationOnMock -> Future.succeededFuture((BidRequest) invocationOnMock.getArgument(1)));
given(ortb2RequestFactory.removeEmptyEids(any(), any()))
Expand Down
Loading
Loading