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

Support Seat Non-bid for DSA Rejections #2990

Merged
merged 2 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
80 changes: 80 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,80 @@
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);
And1sS marked this conversation as resolved.
Show resolved Hide resolved
updatedBidderBids.remove(bidderBid);
}
}

if (bidderBids.size() == updatedBidderBids.size() && seatBid.getWarnings().size() == warnings.size()) {
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
return auctionParticipation;
}

rejectionTracker.restoreFromRejection(updatedBidderBids);
AntoxaAntoxic marked this conversation as resolved.
Show resolved Hide resolved
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
marki1an marked this conversation as resolved.
Show resolved Hide resolved

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
Loading