Skip to content

Commit

Permalink
Core: Support Seat Non-bid for DSA Rejections (#2990)
Browse files Browse the repository at this point in the history
  • Loading branch information
AntoxaAntoxic authored Feb 20, 2024
1 parent 4ec0323 commit 7672f9e
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 231 deletions.
79 changes: 79 additions & 0 deletions src/main/java/org/prebid/server/auction/DsaEnforcer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package org.prebid.server.auction;

import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Regs;
import com.iab.openrtb.response.Bid;
import org.apache.commons.collections4.CollectionUtils;
import org.prebid.server.auction.model.AuctionParticipation;
import org.prebid.server.auction.model.BidRejectionReason;
import org.prebid.server.auction.model.BidRejectionTracker;
import org.prebid.server.auction.model.BidderResponse;
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.proto.openrtb.ext.request.ExtRegs;
import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa;
import org.prebid.server.util.ObjectUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;

public class DsaEnforcer {

private static final String DSA_EXT = "dsa";
private static final Set<Integer> DSA_REQUIRED = Set.of(2, 3);

public AuctionParticipation enforce(BidRequest bidRequest,
AuctionParticipation auctionParticipation,
BidRejectionTracker rejectionTracker) {

final BidderResponse bidderResponse = auctionParticipation.getBidderResponse();
final BidderSeatBid seatBid = ObjectUtil.getIfNotNull(bidderResponse, BidderResponse::getSeatBid);
final List<BidderBid> bidderBids = ObjectUtil.getIfNotNull(seatBid, BidderSeatBid::getBids);

if (CollectionUtils.isEmpty(bidderBids) || !isDsaValidationRequired(bidRequest)) {
return auctionParticipation;
}

final List<BidderBid> updatedBidderBids = new ArrayList<>(bidderBids);
final List<BidderError> warnings = new ArrayList<>(seatBid.getWarnings());

for (BidderBid bidderBid : bidderBids) {
final Bid bid = bidderBid.getBid();

if (!isValid(bid)) {
warnings.add(BidderError.invalidBid("Bid \"%s\" missing DSA".formatted(bid.getId())));
rejectionTracker.reject(bid.getImpid(), BidRejectionReason.GENERAL);
updatedBidderBids.remove(bidderBid);
}
}

if (bidderBids.size() == updatedBidderBids.size()) {
return auctionParticipation;
}

final BidderSeatBid bidderSeatBid = seatBid.toBuilder()
.bids(updatedBidderBids)
.warnings(warnings)
.build();
return auctionParticipation.with(bidderResponse.with(bidderSeatBid));
}

private static boolean isDsaValidationRequired(BidRequest bidRequest) {
return Optional.ofNullable(bidRequest.getRegs())
.map(Regs::getExt)
.map(ExtRegs::getDsa)
.map(ExtRegsDsa::getDsaRequired)
.map(DSA_REQUIRED::contains)
.orElse(false);
}

private boolean isValid(Bid bid) {
final ObjectNode bidExt = bid.getExt();
return bidExt != null && bidExt.hasNonNull(DSA_EXT) && !bidExt.get(DSA_EXT).isEmpty();
}

}
23 changes: 8 additions & 15 deletions src/main/java/org/prebid/server/auction/ExchangeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import com.iab.openrtb.request.Dooh;
import com.iab.openrtb.request.Eid;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.request.Regs;
import com.iab.openrtb.request.Site;
import com.iab.openrtb.request.Source;
import com.iab.openrtb.request.SupplyChain;
Expand Down Expand Up @@ -96,8 +95,6 @@
import org.prebid.server.proto.openrtb.ext.request.ExtDooh;
import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors;
import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
Expand Down Expand Up @@ -170,7 +167,6 @@ public class ExchangeService {
private static final Integer DEFAULT_MULTIBID_LIMIT_MAX = 9;
private static final String EID_ALLOWED_FOR_ALL_BIDDERS = "*";
private static final BigDecimal THOUSAND = BigDecimal.valueOf(1000);
private static final Set<Integer> DSA_REQUIRED = Set.of(2, 3);

private final double logSamplingRate;
private final int timeoutAdjustmentFactor;
Expand All @@ -196,6 +192,7 @@ public class ExchangeService {
private final HttpInteractionLogger httpInteractionLogger;
private final PriceFloorAdjuster priceFloorAdjuster;
private final PriceFloorEnforcer priceFloorEnforcer;
private final DsaEnforcer dsaEnforcer;
private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver;
private final Metrics metrics;
private final Clock clock;
Expand Down Expand Up @@ -227,6 +224,7 @@ public ExchangeService(double logSamplingRate,
HttpInteractionLogger httpInteractionLogger,
PriceFloorAdjuster priceFloorAdjuster,
PriceFloorEnforcer priceFloorEnforcer,
DsaEnforcer dsaEnforcer,
BidAdjustmentFactorResolver bidAdjustmentFactorResolver,
Metrics metrics,
Clock clock,
Expand Down Expand Up @@ -261,6 +259,7 @@ public ExchangeService(double logSamplingRate,
this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger);
this.priceFloorAdjuster = Objects.requireNonNull(priceFloorAdjuster);
this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer);
this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer);
this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver);
this.metrics = Objects.requireNonNull(metrics);
this.clock = Objects.requireNonNull(clock);
Expand Down Expand Up @@ -1482,6 +1481,10 @@ private List<AuctionParticipation> validateAndAdjustBids(List<AuctionParticipati
auctionParticipation,
auctionContext.getAccount(),
auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder())))
.map(auctionParticipation -> dsaEnforcer.enforce(
auctionContext.getBidRequest(),
auctionParticipation,
auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder())))
.toList();
}

Expand Down Expand Up @@ -1525,8 +1528,7 @@ private AuctionParticipation validBidderResponse(AuctionParticipation auctionPar
bid,
bidderResponse.getBidder(),
auctionContext,
aliases,
isDsaValidationRequired(bidRequest));
aliases);

if (validationResult.hasWarnings() || validationResult.hasErrors()) {
errors.add(makeValidationBidderError(bid.getBid(), validationResult));
Expand All @@ -1553,15 +1555,6 @@ private AuctionParticipation validBidderResponse(AuctionParticipation auctionPar
return auctionParticipation.with(resultBidderResponse);
}

private static boolean isDsaValidationRequired(BidRequest bidRequest) {
return Optional.ofNullable(bidRequest.getRegs())
.map(Regs::getExt)
.map(ExtRegs::getDsa)
.map(ExtRegsDsa::getDsaRequired)
.map(DSA_REQUIRED::contains)
.orElse(false);
}

private BidderError makeValidationBidderError(Bid bid, ValidationResult validationResult) {
final String validationErrors = Stream.concat(
validationResult.getErrors().stream().map(message -> "Error: " + message),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum BidRejectionReason {
REJECTED_BY_HOOK(200),
REJECTED_BY_PRIVACY(202),
REJECTED_BY_MEDIA_TYPE(204),
GENERAL(300),
REJECTED_DUE_TO_PRICE_FLOOR(301),
FAILED_TO_REQUEST_BIDS(100),
OTHER_ERROR(100);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.prebid.server.auction.BidResponseCreator;
import org.prebid.server.auction.BidResponsePostProcessor;
import org.prebid.server.auction.DebugResolver;
import org.prebid.server.auction.DsaEnforcer;
import org.prebid.server.auction.ExchangeService;
import org.prebid.server.auction.FpdResolver;
import org.prebid.server.auction.ImplicitParametersExtractor;
Expand Down Expand Up @@ -798,6 +799,7 @@ ExchangeService exchangeService(
HttpInteractionLogger httpInteractionLogger,
PriceFloorAdjuster priceFloorAdjuster,
PriceFloorEnforcer priceFloorEnforcer,
DsaEnforcer dsaEnforcer,
BidAdjustmentFactorResolver bidAdjustmentFactorResolver,
Metrics metrics,
Clock clock,
Expand Down Expand Up @@ -830,6 +832,7 @@ ExchangeService exchangeService(
httpInteractionLogger,
priceFloorAdjuster,
priceFloorEnforcer,
dsaEnforcer,
bidAdjustmentFactorResolver,
metrics,
clock,
Expand Down Expand Up @@ -1066,6 +1069,11 @@ LoggerControlKnob loggerControlKnob(Vertx vertx) {
return new LoggerControlKnob(vertx);
}

@Bean
DsaEnforcer dsaEnforcer() {
return new DsaEnforcer();
}

private static List<String> splitToList(String listAsString) {
return splitToCollection(listAsString, ArrayList::new);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.Banner;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Deal;
Expand Down Expand Up @@ -61,8 +60,6 @@ public class ResponseBidValidator {

private static final String PREBID_EXT = "prebid";
private static final String BIDDER_EXT = "bidder";
private static final String DSA_EXT = "dsa";

private static final String DEALS_ONLY = "dealsonly";

private final BidValidationEnforcement bannerMaxSizeEnforcement;
Expand Down Expand Up @@ -92,8 +89,7 @@ public ResponseBidValidator(BidValidationEnforcement bannerMaxSizeEnforcement,
public ValidationResult validate(BidderBid bidderBid,
String bidder,
AuctionContext auctionContext,
BidderAliases aliases,
boolean isDsaValidationEnabled) {
BidderAliases aliases) {

final Bid bid = bidderBid.getBid();
final BidRequest bidRequest = auctionContext.getBidRequest();
Expand All @@ -105,10 +101,6 @@ public ValidationResult validate(BidderBid bidderBid,
validateTypeSpecific(bidderBid, bidder);
validateCurrency(bidderBid.getBidCurrency());

if (isDsaValidationEnabled) {
validateDsaFor(bid);
}

final Imp correspondingImp = findCorrespondingImp(bid, bidRequest);
if (bidderBid.getType() == BidType.banner) {
warnings.addAll(validateBannerFields(bid, bidder, bidRequest, account, correspondingImp, aliases));
Expand Down Expand Up @@ -164,13 +156,6 @@ private static void validateCurrency(String currency) throws ValidationException
}
}

private void validateDsaFor(Bid bid) throws ValidationException {
final ObjectNode bidExt = bid.getExt();
if (bidExt == null || !bidExt.hasNonNull(DSA_EXT) || bidExt.get(DSA_EXT).isEmpty()) {
throw new ValidationException("Bid \"%s\" missing DSA", bid.getId());
}
}

private Imp findCorrespondingImp(Bid bid, BidRequest bidRequest) throws ValidationException {
return bidRequest.getImp().stream()
.filter(imp -> Objects.equals(imp.getId(), bid.getImpid()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum BidRejectionReason {
REJECTED_BY_HOOK(200),
REJECTED_BY_PRIVACY(202),
REJECTED_BY_MEDIA_TYPE(204),
GENERAL(300),
REJECTED_DUE_TO_PRICE_FLOOR(301),
OTHER_ERROR(100)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.prebid.server.functional.model.response.auction.Dsa as BidDsa
import org.prebid.server.functional.util.PBSUtils
import org.prebid.server.functional.util.privacy.TcfConsent

import static org.prebid.server.functional.model.response.auction.BidRejectionReason.GENERAL
import static org.prebid.server.functional.model.response.auction.ErrorType.GENERIC
import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID
import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.BASIC_ADS
Expand Down Expand Up @@ -159,8 +160,8 @@ class DsaSpec extends PrivacyBaseSpec {

and: "Response should contain an error"
def bidId = bidResponse.seatbid[0].bid[0].id
assert response.ext?.errors[GENERIC]*.code == [5]
assert response.ext?.errors[GENERIC]*.message == ["BidId `$bidId` validation messages: Error: Bid \"$bidId\" missing DSA"]
assert response.ext?.warnings[GENERIC]*.code == [5]
assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\" missing DSA"]

where:
dsaRequired << [DsaRequired.REQUIRED,
Expand Down Expand Up @@ -276,8 +277,47 @@ class DsaSpec extends PrivacyBaseSpec {

and: "Response should contain an error"
def bidId = bidResponse.seatbid[0].bid[0].id
assert response.ext?.errors[GENERIC]*.code == [5]
assert response.ext?.errors[GENERIC]*.message == ["BidId `$bidId` validation messages: Error: Bid \"$bidId\" missing DSA"]
assert response.ext?.warnings[GENERIC]*.code == [5]
assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\" missing DSA"]

where:
dsaRequired << [DsaRequired.REQUIRED,
DsaRequired.REQUIRED_PUBLISHER_IS_ONLINE_PLATFORM]
}

def "Auction request should reject bids without DSA and populate seatNonBid when dsarequired is #dsaRequired"() {
given: "Default bid request with DSA"
def bidRequest = BidRequest.defaultBidRequest.tap {
ext.prebid.returnAllBidStatus = true
regs.ext.dsa = RequestDsa.getDefaultDsa(dsaRequired)
}

and: "Default bidder response without DSA"
def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
seatbid[0].bid[0].ext = new BidExt(dsa: null)
}

and: "Set bidder response"
bidder.setResponse(bidRequest.id, bidResponse)

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

then: "PBS should reject bid"
assert !response.seatbid

and: "PBS response should contain seatNonBid for rejected bids"
assert response.ext.seatnonbid.size() == 1

def seatNonBid = response.ext.seatnonbid[0]
assert seatNonBid.seat == GENERIC.value
assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id
assert seatNonBid.nonBid[0].statusCode == GENERAL

and: "Response should contain an error"
def bidId = bidResponse.seatbid[0].bid[0].id
assert response.ext?.warnings[GENERIC]*.code == [5]
assert response.ext?.warnings[GENERIC]*.message == ["Bid \"$bidId\" missing DSA"]

where:
dsaRequired << [DsaRequired.REQUIRED,
Expand Down
Loading

0 comments on commit 7672f9e

Please sign in to comment.