Skip to content

Commit

Permalink
Core: Bid Adjustments Feature (#3542)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoxaAntoxic authored Nov 13, 2024
1 parent 0970699 commit af13ec8
Show file tree
Hide file tree
Showing 33 changed files with 3,560 additions and 862 deletions.
7 changes: 6 additions & 1 deletion docs/application-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ There are two ways to configure application settings: database and file. This do
operational warning.
- "enforce": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, reject
the bid and log an operational warning.
- `auction.bidadjustments` - configuration JSON for default bid adjustments
- `auction.bidadjustments.mediatype.{banner, video-instream, video-outstream, audio, native, *}.{<BIDDER>, *}.{<DEAL_ID>, *}[]` - array of bid adjustment to be applied to any bid of the provided mediatype, <BIDDER> and <DEAL_ID> (`*` means ANY)
- `auction.bidadjustments.mediatype.*.*.*[].adjtype` - type of the bid adjustment (cpm, multiplier, static)
- `auction.bidadjustments.mediatype.*.*.*[].value` - value of the bid adjustment
- `auction.bidadjustments.mediatype.*.*.*[].currency` - currency of the bid adjustment
- `auction.events.enabled` - enables events for account if true
- `auction.price-floors.enabeled` - enables price floors for account if true. Defaults to true.
- `auction.price-floors.enabled` - enables price floors for account if true. Defaults to true.
- `auction.price-floors.fetch.enabled`- enables data fetch for price floors for account if true. Defaults to false.
- `auction.price-floors.fetch.url` - url to fetch price floors data from.
- `auction.price-floors.fetch.timeout-ms` - timeout for fetching price floors data. Defaults to 5000.
Expand Down
140 changes: 12 additions & 128 deletions src/main/java/org/prebid/server/auction/BidsAdjuster.java
Original file line number Diff line number Diff line change
@@ -1,32 +1,19 @@
package org.prebid.server.auction;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.DecimalNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.response.Bid;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver;
import org.prebid.server.auction.model.AuctionContext;
import org.prebid.server.auction.model.AuctionParticipation;
import org.prebid.server.auction.model.BidderResponse;
import org.prebid.server.bidadjustments.BidAdjustmentsProcessor;
import org.prebid.server.bidder.model.BidderBid;
import org.prebid.server.bidder.model.BidderError;
import org.prebid.server.bidder.model.BidderSeatBid;
import org.prebid.server.currency.CurrencyConversionService;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.floors.PriceFloorEnforcer;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
import org.prebid.server.util.ObjectUtil;
import org.prebid.server.util.PbsUtil;
import org.prebid.server.validation.ResponseBidValidator;
import org.prebid.server.validation.model.ValidationResult;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
Expand All @@ -35,29 +22,20 @@

public class BidsAdjuster {

private static final String ORIGINAL_BID_CPM = "origbidcpm";
private static final String ORIGINAL_BID_CURRENCY = "origbidcur";

private final ResponseBidValidator responseBidValidator;
private final CurrencyConversionService currencyService;
private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver;
private final PriceFloorEnforcer priceFloorEnforcer;
private final BidAdjustmentsProcessor bidAdjustmentsProcessor;
private final DsaEnforcer dsaEnforcer;
private final JacksonMapper mapper;

public BidsAdjuster(ResponseBidValidator responseBidValidator,
CurrencyConversionService currencyService,
BidAdjustmentFactorResolver bidAdjustmentFactorResolver,
PriceFloorEnforcer priceFloorEnforcer,
DsaEnforcer dsaEnforcer,
JacksonMapper mapper) {
BidAdjustmentsProcessor bidAdjustmentsProcessor,
DsaEnforcer dsaEnforcer) {

this.responseBidValidator = Objects.requireNonNull(responseBidValidator);
this.currencyService = Objects.requireNonNull(currencyService);
this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver);
this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer);
this.bidAdjustmentsProcessor = Objects.requireNonNull(bidAdjustmentsProcessor);
this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer);
this.mapper = Objects.requireNonNull(mapper);
}

public List<AuctionParticipation> validateAndAdjustBids(List<AuctionParticipation> auctionParticipations,
Expand All @@ -66,12 +44,18 @@ public List<AuctionParticipation> validateAndAdjustBids(List<AuctionParticipatio

return auctionParticipations.stream()
.map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases))
.map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest()))

.map(auctionParticipation -> bidAdjustmentsProcessor.enrichWithAdjustedBids(
auctionParticipation,
auctionContext.getBidRequest(),
auctionContext.getBidAdjustments()))

.map(auctionParticipation -> priceFloorEnforcer.enforce(
auctionContext.getBidRequest(),
auctionParticipation,
auctionContext.getAccount(),
auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder())))

.map(auctionParticipation -> dsaEnforcer.enforce(
auctionContext.getBidRequest(),
auctionParticipation,
Expand Down Expand Up @@ -137,104 +121,4 @@ private BidderError makeValidationBidderError(Bid bid, ValidationResult validati
final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown");
return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors);
}

private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation,
BidRequest bidRequest) {
if (auctionParticipation.isRequestBlocked()) {
return auctionParticipation;
}

final BidderResponse bidderResponse = auctionParticipation.getBidderResponse();
final BidderSeatBid seatBid = bidderResponse.getSeatBid();

final List<BidderBid> bidderBids = seatBid.getBids();
if (bidderBids.isEmpty()) {
return auctionParticipation;
}

final List<BidderBid> updatedBidderBids = new ArrayList<>(bidderBids.size());
final List<BidderError> errors = new ArrayList<>(seatBid.getErrors());
final String adServerCurrency = bidRequest.getCur().getFirst();

for (final BidderBid bidderBid : bidderBids) {
try {
final BidderBid updatedBidderBid =
updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency);
updatedBidderBids.add(updatedBidderBid);
} catch (PreBidException e) {
errors.add(BidderError.generic(e.getMessage()));
}
}

final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder()
.bids(updatedBidderBids)
.errors(errors)
.build());
return auctionParticipation.with(resultBidderResponse);
}

private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid,
BidderResponse bidderResponse,
BidRequest bidRequest,
String adServerCurrency) {
final Bid bid = bidderBid.getBid();
final String bidCurrency = bidderBid.getBidCurrency();
final BigDecimal price = bid.getPrice();

final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency(
price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency);

final BigDecimal priceAdjustmentFactor =
bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid);
final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency);

final ObjectNode bidExt = bid.getExt();
final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode();

updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency);

final Bid.BidBuilder bidBuilder = bid.toBuilder();
if (adjustedPrice.compareTo(price) != 0) {
bidBuilder.price(adjustedPrice);
}

if (!updatedBidExt.isEmpty()) {
bidBuilder.ext(updatedBidExt);
}

return bidderBid.toBuilder().bid(bidBuilder.build()).build();
}

private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) {
final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest);
if (adjustmentFactors == null) {
return null;
}
final ImpMediaType mediaType = ImpMediaTypeResolver.resolve(
bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType());

return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder);
}

private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) {
final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest);
return prebid != null ? prebid.getBidadjustmentfactors() : null;
}

private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) {
return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0
? price.multiply(priceAdjustmentFactor)
: price;
}

private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) {
addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price));
if (StringUtils.isNotBlank(bidCurrency)) {
addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency));
}
}

private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) {
node.set(propertyName, propertyValue);
}
}
12 changes: 12 additions & 0 deletions src/main/java/org/prebid/server/auction/model/AuctionContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
import org.prebid.server.auction.gpp.model.GppContext;
import org.prebid.server.auction.model.debug.DebugContext;
import org.prebid.server.bidadjustments.model.BidAdjustments;
import org.prebid.server.cache.model.DebugHttpCall;
import org.prebid.server.cookie.UidsCookie;
import org.prebid.server.geolocation.model.GeoInfo;
Expand All @@ -17,6 +18,7 @@
import org.prebid.server.privacy.model.PrivacyContext;
import org.prebid.server.settings.model.Account;

import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -71,6 +73,10 @@ public class AuctionContext {

CachedDebugLog cachedDebugLog;

@JsonIgnore
@Builder.Default
BidAdjustments bidAdjustments = BidAdjustments.of(Collections.emptyMap());

public AuctionContext with(Account account) {
return this.toBuilder().account(account).build();
}
Expand Down Expand Up @@ -124,6 +130,12 @@ public AuctionContext with(GeoInfo geoInfo) {
.build();
}

public AuctionContext with(BidAdjustments bidAdjustments) {
return this.toBuilder()
.bidAdjustments(bidAdjustments)
.build();
}

public AuctionContext withRequestRejected() {
return this.toBuilder()
.requestRejected(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.prebid.server.auction.model.AuctionStoredResult;
import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory;
import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager;
import org.prebid.server.bidadjustments.BidAdjustmentsRetriever;
import org.prebid.server.cookie.CookieDeprecationService;
import org.prebid.server.exception.InvalidRequestException;
import org.prebid.server.json.JacksonMapper;
Expand Down Expand Up @@ -50,6 +51,7 @@ public class AuctionRequestFactory {
private final JacksonMapper mapper;
private final OrtbTypesResolver ortbTypesResolver;
private final GeoLocationServiceWrapper geoLocationServiceWrapper;
private final BidAdjustmentsRetriever bidAdjustmentsRetriever;

private static final String ENDPOINT = Endpoint.openrtb2_auction.value();

Expand All @@ -66,7 +68,8 @@ public AuctionRequestFactory(long maxRequestSize,
AuctionPrivacyContextFactory auctionPrivacyContextFactory,
DebugResolver debugResolver,
JacksonMapper mapper,
GeoLocationServiceWrapper geoLocationServiceWrapper) {
GeoLocationServiceWrapper geoLocationServiceWrapper,
BidAdjustmentsRetriever bidAdjustmentsRetriever) {

this.maxRequestSize = maxRequestSize;
this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory);
Expand All @@ -82,6 +85,7 @@ public AuctionRequestFactory(long maxRequestSize,
this.debugResolver = Objects.requireNonNull(debugResolver);
this.mapper = Objects.requireNonNull(mapper);
this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper);
this.bidAdjustmentsRetriever = Objects.requireNonNull(bidAdjustmentsRetriever);
}

/**
Expand Down Expand Up @@ -142,6 +146,8 @@ public Future<AuctionContext> enrichAuctionContext(AuctionContext initialContext
.compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext)
.map(auctionContext::with))

.map(auctionContext -> auctionContext.with(bidAdjustmentsRetriever.retrieve(auctionContext)))

.compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext)
.map(auctionContext::with))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.prebid.server.bidadjustments;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.bidadjustments.model.BidAdjustmentType;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule;
import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
import org.prebid.server.validation.ValidationException;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class BidAdjustmentRulesValidator {

public static final Set<String> SUPPORTED_MEDIA_TYPES = Set.of(
BidAdjustmentsResolver.WILDCARD,
ImpMediaType.banner.toString(),
ImpMediaType.audio.toString(),
ImpMediaType.video_instream.toString(),
ImpMediaType.video_outstream.toString(),
ImpMediaType.xNative.toString());

private BidAdjustmentRulesValidator() {

}

public static void validate(ExtRequestBidAdjustments bidAdjustments) throws ValidationException {
if (bidAdjustments == null) {
return;
}

final Map<String, Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>>> mediatypes =
bidAdjustments.getMediatype();

if (MapUtils.isEmpty(mediatypes)) {
return;
}

for (String mediatype : mediatypes.keySet()) {
if (SUPPORTED_MEDIA_TYPES.contains(mediatype)) {
final Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>> bidders = mediatypes.get(mediatype);
if (MapUtils.isEmpty(bidders)) {
throw new ValidationException("no bidders found in %s".formatted(mediatype));
}
for (String bidder : bidders.keySet()) {
final Map<String, List<ExtRequestBidAdjustmentsRule>> deals = bidders.get(bidder);

if (MapUtils.isEmpty(deals)) {
throw new ValidationException("no deals found in %s.%s".formatted(mediatype, bidder));
}

for (String dealId : deals.keySet()) {
final String path = "%s.%s.%s".formatted(mediatype, bidder, dealId);
validateRules(deals.get(dealId), path);
}
}
}
}
}

private static void validateRules(List<ExtRequestBidAdjustmentsRule> rules,
String path) throws ValidationException {

if (rules == null) {
throw new ValidationException("no bid adjustment rules found in %s".formatted(path));
}

for (ExtRequestBidAdjustmentsRule rule : rules) {
final BidAdjustmentType type = rule.getAdjType();
final String currency = rule.getCurrency();
final BigDecimal value = rule.getValue();

final boolean isNotSpecifiedCurrency = StringUtils.isBlank(currency);

final boolean unknownType = type == null || type == BidAdjustmentType.UNKNOWN;

final boolean invalidCpm = type == BidAdjustmentType.CPM
&& (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE));

final boolean invalidMultiplier = type == BidAdjustmentType.MULTIPLIER
&& isValueNotInRange(value, 0, 100);

final boolean invalidStatic = type == BidAdjustmentType.STATIC
&& (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE));

if (unknownType || invalidCpm || invalidMultiplier || invalidStatic) {
throw new ValidationException("the found rule %s in %s is invalid".formatted(rule, path));
}
}
}

private static boolean isValueNotInRange(BigDecimal value, int minValue, int maxValue) {
return value == null
|| value.compareTo(BigDecimal.valueOf(minValue)) < 0
|| value.compareTo(BigDecimal.valueOf(maxValue)) >= 0;
}
}
Loading

0 comments on commit af13ec8

Please sign in to comment.