From 91a6cf278acf177f88e523394aaa03a8559bfd96 Mon Sep 17 00:00:00 2001 From: DGarbar Date: Fri, 26 Feb 2021 12:15:09 +0200 Subject: [PATCH] Support MultiBid feature Detailed https://github.com/prebid/prebid-server/issues/1715 Refactor winningBid logic. Added IT test. --- .../server/auction/BidResponseCreator.java | 243 +++++++++------ .../server/auction/BidResponseReducer.java | 85 ------ .../server/auction/ExchangeService.java | 18 +- .../server/auction/WinningBidComparator.java | 28 ++ .../auction/model/TargetingBidInfo.java | 21 ++ .../openrtb/ext/request/ExtRequestPrebid.java | 5 + .../ext/request/ExtRequestPrebidMultiBid.java | 28 ++ .../openrtb/ext/response/ExtBidPrebid.java | 3 + .../spring/config/ServiceConfiguration.java | 10 +- .../auction/BidResponseCreatorTest.java | 286 ++++++++++++------ .../auction/BidResponseReducerTest.java | 116 ------- .../server/auction/ExchangeServiceTest.java | 40 ++- .../auction/WinningBidComparatorTest.java | 81 +++++ .../org/prebid/server/it/ApplicationTest.java | 50 +++ .../test-appnexus-bid-request-1.json | 129 ++++++++ .../test-appnexus-bid-response-1.json | 78 +++++ ...test-auction-rubicon-appnexus-request.json | 162 ++++++++++ ...est-auction-rubicon-appnexus-response.json | 234 ++++++++++++++ .../test-cache-matcher-rubicon-appnexus.json | 10 + .../test-cache-rubicon-appnexus-request.json | 129 ++++++++ .../test-rubicon-bid-request-1.json | 109 +++++++ .../test-rubicon-bid-response-1.json | 74 +++++ 22 files changed, 1542 insertions(+), 397 deletions(-) delete mode 100644 src/main/java/org/prebid/server/auction/BidResponseReducer.java create mode 100644 src/main/java/org/prebid/server/auction/WinningBidComparator.java create mode 100644 src/main/java/org/prebid/server/auction/model/TargetingBidInfo.java create mode 100644 src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidMultiBid.java delete mode 100644 src/test/java/org/prebid/server/auction/BidResponseReducerTest.java create mode 100644 src/test/java/org/prebid/server/auction/WinningBidComparatorTest.java create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-request-1.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-response-1.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-response.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-matcher-rubicon-appnexus.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-rubicon-appnexus-request.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-request-1.json create mode 100644 src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-response-1.json diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index 0390eda9b7f..6230e26f16d 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -27,6 +27,7 @@ import org.prebid.server.auction.model.BidInfo; import org.prebid.server.auction.model.BidRequestCacheInfo; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.auction.model.TargetingBidInfo; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.BidderSeatBid; @@ -53,6 +54,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.proto.openrtb.ext.response.CacheAsset; @@ -75,7 +77,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.EnumMap; import java.util.HashMap; import java.util.HashSet; @@ -98,13 +99,14 @@ public class BidResponseCreator { private static final String CACHE = "cache"; private static final String PREBID_EXT = "prebid"; private static final String SKADN_PROPERTY = "skadn"; + private static final int DEFAULT_BID_LIMIT_MIN = 1; + private static final int DEFAULT_BID_LIMIT_MAX = 9; private final CacheService cacheService; private final BidderCatalog bidderCatalog; private final VastModifier vastModifier; private final EventsService eventsService; private final StoredRequestProcessor storedRequestProcessor; - private final BidResponseReducer bidResponseReducer; private final IdGenerator bidIdGenerator; private final int truncateAttrChars; private final Clock clock; @@ -113,13 +115,14 @@ public class BidResponseCreator { private final String cacheHost; private final String cachePath; private final String cacheAssetUrlTemplate; + private final WinningBidComparator winningBidComparator; public BidResponseCreator(CacheService cacheService, BidderCatalog bidderCatalog, VastModifier vastModifier, EventsService eventsService, StoredRequestProcessor storedRequestProcessor, - BidResponseReducer bidResponseReducer, + WinningBidComparator winningBidComparator, IdGenerator bidIdGenerator, int truncateAttrChars, Clock clock, @@ -130,7 +133,7 @@ public BidResponseCreator(CacheService cacheService, this.vastModifier = Objects.requireNonNull(vastModifier); this.eventsService = Objects.requireNonNull(eventsService); this.storedRequestProcessor = Objects.requireNonNull(storedRequestProcessor); - this.bidResponseReducer = Objects.requireNonNull(bidResponseReducer); + this.winningBidComparator = Objects.requireNonNull(winningBidComparator); this.bidIdGenerator = Objects.requireNonNull(bidIdGenerator); this.truncateAttrChars = validateTruncateAttrChars(truncateAttrChars); this.clock = Objects.requireNonNull(clock); @@ -148,6 +151,7 @@ public BidResponseCreator(CacheService cacheService, Future create(List bidderResponses, AuctionContext auctionContext, BidRequestCacheInfo cacheInfo, + Map bidderToMultiBids, boolean debugEnabled) { final long auctionTimestamp = auctionTimestamp(auctionContext); @@ -174,6 +178,7 @@ Future create(List bidderResponses, bidderResponses, auctionContext, cacheInfo, + bidderToMultiBids, auctionTimestamp, debugEnabled); } @@ -197,34 +202,34 @@ private static boolean isEmptyBidderResponses(List bidderRespons private Future cacheBidsAndCreateResponse(List bidderResponses, AuctionContext auctionContext, BidRequestCacheInfo cacheInfo, + Map bidderToMultiBids, long auctionTimestamp, boolean debugEnabled) { - final BidRequest bidRequest = auctionContext.getBidRequest(); - final List updatedBidderResponses = bidderResponses.stream() - .map(bidResponseReducer::removeRedundantBids) - .collect(Collectors.toList()); - final List imps = bidRequest.getImp(); - final Map> bidResponseToBidInfos = updatedBidderResponses.stream() - .collect(Collectors.toMap(Function.identity(), bidderResponse -> toBidInfo(bidderResponse, imps))); + final Map> bidderResponseToTargetingBidInfos = + toBidderResponseWithTargetingBidInfos(bidderResponses, imps, bidderToMultiBids); - final Set bidInfos = bidResponseToBidInfos.values().stream() + final Set bidInfos = bidderResponseToTargetingBidInfos.values().stream() .filter(CollectionUtils::isNotEmpty) .flatMap(Collection::stream) + .map(TargetingBidInfo::getBidInfo) .collect(Collectors.toSet()); final ExtRequestTargeting targeting = targeting(bidRequest); - final Set winningBids = newOrEmptySet(targeting); - final Set winningBidsByBidder = newOrEmptySet(targeting); // determine winning bids only if targeting is present + Set winningBidInfos = null; if (targeting != null) { - populateWinningBids(bidInfos, winningBids, winningBidsByBidder); + winningBidInfos = bidderResponseToTargetingBidInfos.values().stream() + .flatMap(Collection::stream) + .filter(TargetingBidInfo::isWinningBid) + .map(TargetingBidInfo::getBidInfo) + .collect(Collectors.toSet()); } - final Set bidsToCache = cacheInfo.isShouldCacheWinningBidsOnly() ? winningBids : bidInfos; + final Set bidsToCache = cacheInfo.isShouldCacheWinningBidsOnly() ? winningBidInfos : bidInfos; final EventsContext eventsContext = EventsContext.builder() .enabledForAccount(eventsEnabledForAccount(auctionContext)) @@ -236,11 +241,9 @@ private Future cacheBidsAndCreateResponse(List bidd return cacheBids(bidsToCache, auctionContext, cacheInfo, eventsContext) .compose(cacheResult -> videoStoredDataResult(auctionContext) .map(videoStoredDataResult -> toBidResponse( - bidResponseToBidInfos, + bidderResponseToTargetingBidInfos, auctionContext, targeting, - winningBids, - winningBidsByBidder, cacheInfo, cacheResult, videoStoredDataResult, @@ -255,6 +258,68 @@ private static ExtRequestTargeting targeting(BidRequest bidRequest) { return prebid != null ? prebid.getTargeting() : null; } + private Map> toBidderResponseWithTargetingBidInfos( + List bidderResponses, + List imps, + Map bidderToMultiBids) { + + final Map> bidderResponseToReducedBidInfos = bidderResponses.stream() + .collect(Collectors.toMap( + Function.identity(), + bidderResponse -> toSortedMultiBidInfo(bidderResponse, imps, bidderToMultiBids))); + + final Map>> impIdToBidderToBidInfos = bidderResponseToReducedBidInfos.values() + .stream() + .flatMap(Collection::stream) + .collect(Collectors.groupingBy( + bidInfo -> bidInfo.getCorrespondingImp().getId(), + Collectors.groupingBy(BidInfo::getBidder))); + + // Best bids from bidders for imp + final Set winningBids = new HashSet<>(); + // All bids from bidder for imp + final Set winningBidsByBidder = new HashSet<>(); + + for (Map> bidderToBidInfos : impIdToBidderToBidInfos.values()) { + + bidderToBidInfos.values().forEach(winningBidsByBidder::addAll); + + bidderToBidInfos.values().stream() + .flatMap(Collection::stream) + .max(winningBidComparator) + .ifPresent(winningBids::add); + } + + return bidderResponseToReducedBidInfos.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + responseToBidInfos -> toTargetingBidInfo( + responseToBidInfos.getValue(), + responseToBidInfos.getKey().getBidder(), + bidderToMultiBids, + winningBids, + winningBidsByBidder))); + } + + private List toSortedMultiBidInfo(BidderResponse bidderResponse, + List imps, + Map bidderToMultiBids) { + final List bidInfos = toBidInfo(bidderResponse, imps); + final Map> impIdToBidInfos = bidInfos.stream() + .collect(Collectors.groupingBy(bidInfo -> bidInfo.getCorrespondingImp().getId())); + + final ExtRequestPrebidMultiBid multiBid = bidderToMultiBids.get(bidderResponse.getBidder()); + final Integer maxBids = multiBid != null ? multiBid.getMaxBids() : null; + final int bidLimit = maxBids == null || maxBids < DEFAULT_BID_LIMIT_MIN + ? DEFAULT_BID_LIMIT_MIN + : maxBids > 9 ? DEFAULT_BID_LIMIT_MAX : maxBids; + + return impIdToBidInfos.values().stream() + .map(impIdBidInfos -> sortReducedBidInfo(impIdBidInfos, bidLimit)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + private List toBidInfo(BidderResponse bidderResponse, List imps) { return Stream.of(bidderResponse) .map(BidderResponse::getSeatBid) @@ -286,6 +351,62 @@ private static Imp correspondingImp(Bid bid, List imps) { () -> new PreBidException(String.format("Bid with impId %s doesn't have matched imp", impId))); } + private List sortReducedBidInfo(List bidInfos, int limit) { + return bidInfos.stream() + .sorted(winningBidComparator.reversed()) + .limit(limit) + .collect(Collectors.toList()); + } + + private List toTargetingBidInfo(List bidderBidInfos, + String bidder, + Map bidderToMultiBids, + Set winningBids, + Set winningBidsByBidder) { + final Map> impIdToBidInfos = bidderBidInfos.stream() + .collect(Collectors.groupingBy(bidInfo -> bidInfo.getCorrespondingImp().getId())); + + return impIdToBidInfos.values().stream() + .map(bidInfos -> createTargetingBidInfo(bidInfos, bidder, bidderToMultiBids, winningBids, + winningBidsByBidder)) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + } + + private List createTargetingBidInfo(List bidderImpIdBidInfos, + String bidder, + Map bidderToMultiBids, + Set winningBids, + Set winningBidsByBidder) { + final List targetingBidInfos = new ArrayList<>(); + + final ExtRequestPrebidMultiBid multiBid = bidderToMultiBids.get(bidder); + final String bidderCodePrefix = multiBid != null ? multiBid.getTargetBidderCodePrefix() : null; + + final int multiBidSize = bidderImpIdBidInfos.size(); + for (int i = 0; i < multiBidSize; i++) { + // first bid have highest value and can't be extra bid. + final boolean isFirstBid = i == 0; + final String targetingBidderCode = isFirstBid + ? bidder + : bidderCodePrefix == null ? null : String.format("%s%s", bidderCodePrefix, i + 1); + + final BidInfo bidInfo = bidderImpIdBidInfos.get(i); + final TargetingBidInfo targetingBidInfo = TargetingBidInfo.builder() + .bidInfo(bidInfo) + .isTargetingEnabled(targetingBidderCode != null) + .isBidderWinningBid(winningBidsByBidder.contains(bidInfo)) + .isWinningBid(winningBids.contains(bidInfo)) + .isAddTargetBidderCode(targetingBidderCode != null && multiBidSize > 1) + .bidderCode(targetingBidderCode) + .build(); + + targetingBidInfos.add(targetingBidInfo); + } + + return targetingBidInfos; + } + /** * Extracts auction timestamp from {@link ExtRequest} or get it from {@link Clock} if it is null. */ @@ -321,45 +442,6 @@ private ExtBidResponse toExtBidResponse(Collection bidderRespons ExtBidResponsePrebid.of(auctionTimestamp)); } - /** - * Returns new {@link HashSet} in case of existing keywordsCreator or empty collection if null. - */ - private static Set newOrEmptySet(ExtRequestTargeting targeting) { - return targeting != null ? new HashSet<>() : Collections.emptySet(); - } - - /** - * Populates 2 input sets: - *

- * - winning bids for each impId (ad unit code) through all bidder responses. - *
- * - winning bids for each impId but for separate bidder. - *

- * Winning bid is the one with the highest price. - */ - private static void populateWinningBids(Set bidInfos, - Set winningBids, - Set winningBidsByBidder) { - final Map> impIdToBidderToBidInfo = bidInfos.stream() - .collect(Collectors.groupingBy( - bidInfo -> bidInfo.getCorrespondingImp().getId(), - Collectors.toMap(BidInfo::getBidder, Function.identity(), BidResponseCreator::winningBidInfo))); - - for (Map bidderToBidInfo : impIdToBidderToBidInfo.values()) { - winningBidsByBidder.addAll(bidderToBidInfo.values()); - - bidderToBidInfo.values().stream() - .max(Comparator.comparing(o -> o.getBid().getPrice())) - .ifPresent(winningBids::add); - } - } - - private static BidInfo winningBidInfo(BidInfo bidInfo1, BidInfo bidInfo2) { - final Bid bid1 = bidInfo1.getBid(); - final Bid bid2 = bidInfo2.getBid(); - return bid1.getPrice().compareTo(bid2.getPrice()) > 0 ? bidInfo1 : bidInfo2; - } - /** * Corresponds cacheId (or null if not present) to each {@link Bid}. */ @@ -590,11 +672,9 @@ private static Map toResponseTimes(Collection b /** * Returns {@link BidResponse} based on list of {@link BidderResponse}s and {@link CacheServiceResult}. */ - private BidResponse toBidResponse(Map> bidResponseToBidInfos, + private BidResponse toBidResponse(Map> bidderResponseToTargetingBidInfos, AuctionContext auctionContext, ExtRequestTargeting targeting, - Set winningBids, - Set winningBidsByBidder, BidRequestCacheInfo requestCacheInfo, CacheServiceResult cacheResult, VideoStoredDataResult videoStoredDataResult, @@ -606,14 +686,12 @@ private BidResponse toBidResponse(Map> bidResponse final Account account = auctionContext.getAccount(); final Map> bidErrors = new HashMap<>(); - final List seatBids = bidResponseToBidInfos.values().stream() + final List seatBids = bidderResponseToTargetingBidInfos.values().stream() .filter(CollectionUtils::isNotEmpty) - .map(bidInfos -> toSeatBid( - bidInfos, + .map(targetingBidInfos -> toSeatBid( + targetingBidInfos, targeting, bidRequest, - winningBids, - winningBidsByBidder, requestCacheInfo, cacheResult.getCacheBids(), videoStoredDataResult, @@ -623,7 +701,7 @@ private BidResponse toBidResponse(Map> bidResponse .collect(Collectors.toList()); final ExtBidResponse extBidResponse = toExtBidResponse( - bidResponseToBidInfos.keySet(), + bidderResponseToTargetingBidInfos.keySet(), auctionContext, cacheResult, videoStoredDataResult, @@ -685,11 +763,9 @@ private boolean checkEchoVideoAttrs(Imp imp) { * Creates an OpenRTB {@link SeatBid} for a bidder. It will contain all the bids supplied by a bidder and a "bidder" * extension field populated. */ - private SeatBid toSeatBid(List bidInfos, + private SeatBid toSeatBid(List targetingBidInfos, ExtRequestTargeting targeting, BidRequest bidRequest, - Set winningBids, - Set winningBidsByBidder, BidRequestCacheInfo requestCacheInfo, Map bidToCacheInfo, VideoStoredDataResult videoStoredDataResult, @@ -697,19 +773,18 @@ private SeatBid toSeatBid(List bidInfos, Map> bidErrors, EventsContext eventsContext) { - final String bidder = bidInfos.stream() + final String bidder = targetingBidInfos.stream() + .map(TargetingBidInfo::getBidInfo) .map(BidInfo::getBidder) .findFirst() // Should never occur .orElseThrow(() -> new IllegalArgumentException("Bidder was not defined for bidInfo")); - final List bids = bidInfos.stream() - .map(bidInfo -> toBid( - bidInfo, + final List bids = targetingBidInfos.stream() + .map(targetingBidInfo -> toBid( + targetingBidInfo, targeting, bidRequest, - winningBids, - winningBidsByBidder, requestCacheInfo, bidToCacheInfo, videoStoredDataResult.getImpIdToStoredVideo(), @@ -729,25 +804,20 @@ private SeatBid toSeatBid(List bidInfos, /** * Returns an OpenRTB {@link Bid} with "prebid" and "bidder" extension fields populated. */ - private Bid toBid(BidInfo bidInfo, + private Bid toBid(TargetingBidInfo targetingBidInfo, ExtRequestTargeting targeting, BidRequest bidRequest, - Set winningBids, - Set winningBidsByBidder, BidRequestCacheInfo requestCacheInfo, Map bidsWithCacheIds, Map impIdToStoredVideo, Account account, EventsContext eventsContext, Map> bidErrors) { + final BidInfo bidInfo = targetingBidInfo.getBidInfo(); final Bid bid = bidInfo.getBid(); final BidType bidType = bidInfo.getBidType(); final String bidder = bidInfo.getBidder(); - // preliminary variables are needed because bid is changing below, so we can lost it in winning bids sets - final boolean isWinningBid = winningBids.contains(bidInfo); - final boolean isWinningBidByBidder = winningBidsByBidder.contains(bidInfo); - final CacheInfo cacheInfo = bidsWithCacheIds.get(bid); final String cacheId = cacheInfo != null ? cacheInfo.getCacheId() : null; final String videoCacheId = cacheInfo != null ? cacheInfo.getVideoCacheId() : null; @@ -779,11 +849,13 @@ private Bid toBid(BidInfo bidInfo, } final Map targetingKeywords; - if (targeting != null && isWinningBidByBidder) { + final String bidderCode = targetingBidInfo.getBidderCode(); + if (targeting != null && targetingBidInfo.isTargetingEnabled() && targetingBidInfo.isBidderWinningBid()) { final TargetingKeywordsCreator keywordsCreator = resolveKeywordsCreator(bidType, targeting, isApp, bidRequest, account); - targetingKeywords = keywordsCreator.makeFor(bid, bidder, isWinningBid, cacheId, bidType.getName(), + final boolean isWinningBid = targetingBidInfo.isWinningBid(); + targetingKeywords = keywordsCreator.makeFor(bid, bidderCode, isWinningBid, cacheId, bidType.getName(), videoCacheId); } else { targetingKeywords = null; @@ -801,6 +873,7 @@ private Bid toBid(BidInfo bidInfo, .bidid(bidInfo.getGeneratedBidId()) .type(bidType) .targeting(targetingKeywords) + .targetBidderCode(targetingBidInfo.isAddTargetBidderCode() ? bidderCode : null) .cache(cache) .storedRequestAttributes(storedVideo) .events(events) diff --git a/src/main/java/org/prebid/server/auction/BidResponseReducer.java b/src/main/java/org/prebid/server/auction/BidResponseReducer.java deleted file mode 100644 index 5d1635abdd2..00000000000 --- a/src/main/java/org/prebid/server/auction/BidResponseReducer.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.prebid.server.auction; - -import com.iab.openrtb.response.Bid; -import org.apache.commons.collections4.ListUtils; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.auction.model.BidderResponse; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderSeatBid; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Class for removing bids from response for the same bidder-imp pair. - */ -public class BidResponseReducer { - - /** - * Removes {@link Bid}s with the same impId taking into account if {@link Bid} has deal. - *

- * Returns given list of {@link BidderResponse}s if {@link Bid}s have different impIds. - */ - public BidderResponse removeRedundantBids(BidderResponse bidderResponse) { - final List bidderBids = ListUtils.emptyIfNull(bidderResponse.getSeatBid().getBids()); - final Map> impIdToBidderBids = bidderBids.stream() - .collect(Collectors.groupingBy(bidderBid -> bidderBid.getBid().getImpid())); - - final Set updatedBidderBids = impIdToBidderBids.values().stream() - .map(BidResponseReducer::removeRedundantBidsForImp) - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - - if (bidderBids.size() == updatedBidderBids.size()) { - return bidderResponse; - } - - return updateBidderResponse(bidderResponse, updatedBidderBids); - } - - private static List removeRedundantBidsForImp(List bidderBids) { - return bidderBids.size() > 1 ? reduceBidsByImpId(bidderBids) : bidderBids; - } - - private static List reduceBidsByImpId(List bidderBids) { - return bidderBids.stream().anyMatch(bidderBid -> bidderBid.getBid().getDealid() != null) - ? removeRedundantDealsBids(bidderBids) - : removeRedundantForNonDealBids(bidderBids); - } - - private static List removeRedundantDealsBids(List bidderBids) { - final List dealBidderBids = bidderBids.stream() - .filter(bidderBid -> StringUtils.isNotBlank(bidderBid.getBid().getDealid())) - .collect(Collectors.toList()); - - return Collections.singletonList(getHighestPriceBid(bidderBids, dealBidderBids)); - } - - private static List removeRedundantForNonDealBids(List bidderBids) { - return Collections.singletonList(getHighestPriceBid(bidderBids, bidderBids)); - } - - private static BidderBid getHighestPriceBid(List bidderBids, List dealBidderBids) { - return dealBidderBids.stream() - .max(Comparator.comparing(bidderBid -> bidderBid.getBid().getPrice(), Comparator.naturalOrder())) - .orElse(bidderBids.get(0)); - } - - private static BidderResponse updateBidderResponse(BidderResponse bidderResponse, - Set updatedBidderBids) { - - final BidderSeatBid seatBid = bidderResponse.getSeatBid(); - final BidderSeatBid updatedSeatBid = BidderSeatBid.of( - new ArrayList<>(updatedBidderBids), - seatBid.getHttpCalls(), - seatBid.getErrors()); - - return BidderResponse.of(bidderResponse.getBidder(), updatedSeatBid, bidderResponse.getResponseTime()); - } -} diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index b480ddc1b4f..c726b40aea3 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -49,6 +49,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidBidderConfig; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCache; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; @@ -69,6 +70,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -148,6 +150,7 @@ public Future holdAuction(AuctionContext context) { final String publisherId = account.getId(); final BidRequestCacheInfo cacheInfo = bidRequestCacheInfo(bidRequest); final boolean debugEnabled = isDebugEnabled(bidRequest); + final Map bidderToMultiBids = bidderToMultiBids(bidRequest); return storedResponseProcessor.getStoredResponseResult(bidRequest.getImp(), aliases, timeout) .map(storedResponseResult -> populateStoredResponse(storedResponseResult, storedResponse)) @@ -173,6 +176,7 @@ public Future holdAuction(AuctionContext context) { bidderResponses, context, cacheInfo, + bidderToMultiBids, debugEnabled)) .compose(bidResponse -> bidResponsePostProcessor.postProcess( context.getRoutingContext(), uidsCookie, bidRequest, bidResponse, account)); @@ -249,6 +253,17 @@ private static ExtRequestPrebid extRequestPrebid(BidRequest bidRequest) { return requestExt != null ? requestExt.getPrebid() : null; } + private static Map bidderToMultiBids(BidRequest bidRequest) { + final ExtRequestPrebid extRequestPrebid = extRequestPrebid(bidRequest); + final List multiBids = extRequestPrebid != null + ? extRequestPrebid.getMultibid() + : null; + return multiBids == null + ? Collections.emptyMap() + : multiBids.stream() + .collect(Collectors.toMap(ExtRequestPrebidMultiBid::getBidder, Function.identity())); + } + /** * Populates storedResponse parameter with stored {@link List} and returns {@link List} for which * request to bidders should be performed. @@ -529,8 +544,7 @@ private List getBidderRequests(List bidderPr * Extracts a map of bidders to their arguments from {@link ObjectNode} prebid.bidders. */ private static Map bidderToPrebidBidders(BidRequest bidRequest) { - final ExtRequest requestExt = bidRequest.getExt(); - final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); + final ExtRequestPrebid prebid = extRequestPrebid(bidRequest); final ObjectNode bidders = prebid == null ? null : prebid.getBidders(); if (bidders == null || bidders.isNull()) { diff --git a/src/main/java/org/prebid/server/auction/WinningBidComparator.java b/src/main/java/org/prebid/server/auction/WinningBidComparator.java new file mode 100644 index 00000000000..65c5bb56ed5 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/WinningBidComparator.java @@ -0,0 +1,28 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.request.Imp; +import org.prebid.server.auction.model.BidInfo; + +import java.util.Comparator; +import java.util.Objects; + +/** + * Re + */ +public class WinningBidComparator implements Comparator { + + private final Comparator priceComparator = Comparator.comparing(o -> o.getBid().getPrice()); + + @Override + public int compare(BidInfo bidInfo1, BidInfo bidInfo2) { + final Imp imp = bidInfo1.getCorrespondingImp(); + // this should never happen + if (!Objects.equals(imp, bidInfo2.getCorrespondingImp())) { + throw new IllegalStateException( + String.format("Error while determining winning bid: " + + "Multiple bids for was found for impId: %s", imp.getId())); + } + + return priceComparator.compare(bidInfo1, bidInfo2); + } +} diff --git a/src/main/java/org/prebid/server/auction/model/TargetingBidInfo.java b/src/main/java/org/prebid/server/auction/model/TargetingBidInfo.java new file mode 100644 index 00000000000..319dca3f272 --- /dev/null +++ b/src/main/java/org/prebid/server/auction/model/TargetingBidInfo.java @@ -0,0 +1,21 @@ +package org.prebid.server.auction.model; + +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class TargetingBidInfo { + + BidInfo bidInfo; + + String bidderCode; + + boolean isTargetingEnabled; + + boolean isWinningBid; + + boolean isBidderWinningBid; + + boolean isAddTargetBidderCode; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java index b687825d550..7e19569bbec 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java @@ -109,4 +109,9 @@ public class ExtRequestPrebid { * Defines the contract for bidrequest.ext.prebid.channel */ ExtRequestPrebidChannel channel; + + /** + * Defines the contract for bidrequest.ext.prebid.multibid + */ + List multibid; } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidMultiBid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidMultiBid.java new file mode 100644 index 00000000000..cb567fa1d12 --- /dev/null +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidMultiBid.java @@ -0,0 +1,28 @@ +package org.prebid.server.proto.openrtb.ext.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +/** + * Defines the contract for bidrequest.ext.prebid.targeting + */ +@Value(staticConstructor = "of") +public class ExtRequestPrebidMultiBid { + + /** + * Defines the contract for bidrequest.ext.prebid.multibid.bidder + */ + String bidder; + + /** + * Defines the contract for bidrequest.ext.prebid.multibid.maxbids + */ + @JsonProperty("maxbids") + Integer maxBids; + + /** + * Defines the contract for bidrequest.ext.prebid.multibid.targetbiddercodeprefix + */ + @JsonProperty("targetbiddercodeprefix") + String targetBidderCodePrefix; +} diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java index 6099f23fc52..98c3a21c4bc 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebid.java @@ -20,6 +20,9 @@ public class ExtBidPrebid { Map targeting; + @JsonProperty("targetbiddercode") + String targetBidderCode; + ExtResponseCache cache; @JsonProperty("storedrequestattributes") diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java index 26c1cb786b9..9dfaf7abfba 100644 --- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java +++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java @@ -11,7 +11,6 @@ import org.prebid.server.auction.AuctionRequestFactory; import org.prebid.server.auction.BidResponseCreator; import org.prebid.server.auction.BidResponsePostProcessor; -import org.prebid.server.auction.BidResponseReducer; import org.prebid.server.auction.ExchangeService; import org.prebid.server.auction.FpdResolver; import org.prebid.server.auction.ImplicitParametersExtractor; @@ -26,6 +25,7 @@ import org.prebid.server.auction.VideoRequestFactory; import org.prebid.server.auction.VideoResponseFactory; import org.prebid.server.auction.VideoStoredRequestProcessor; +import org.prebid.server.auction.WinningBidComparator; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderDeps; import org.prebid.server.bidder.BidderErrorNotifier; @@ -457,7 +457,7 @@ BidResponseCreator bidResponseCreator( VastModifier vastModifier, EventsService eventsService, StoredRequestProcessor storedRequestProcessor, - BidResponseReducer bidResponseReducer, + WinningBidComparator winningBidComparator, IdGenerator bidIdGenerator, @Value("${settings.targeting.truncate-attr-chars}") int truncateAttrChars, Clock clock, @@ -469,7 +469,7 @@ BidResponseCreator bidResponseCreator( vastModifier, eventsService, storedRequestProcessor, - bidResponseReducer, + winningBidComparator, bidIdGenerator, truncateAttrChars, clock, @@ -533,8 +533,8 @@ StoredRequestProcessor storedRequestProcessor( } @Bean - BidResponseReducer bidResponseReducer() { - return new BidResponseReducer(); + WinningBidComparator winningBidComparator() { + return new WinningBidComparator(); } @Bean diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 12cabbdbeab..9ae9373a10b 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -57,6 +57,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidChannel; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting; import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest; import org.prebid.server.proto.openrtb.ext.response.BidType; @@ -115,6 +116,7 @@ public class BidResponseCreatorTest extends VertxTest { private static final BidRequestCacheInfo CACHE_INFO = BidRequestCacheInfo.builder().build(); + private static final Map MULTI_BIDS = emptyMap(); private static final String IMP_ID = "impId1"; private static final String BID_ADM = "adm"; @@ -134,10 +136,10 @@ public class BidResponseCreatorTest extends VertxTest { @Mock private StoredRequestProcessor storedRequestProcessor; @Mock - private BidResponseReducer bidResponseReducer; - @Mock private IdGenerator idGenerator; + private WinningBidComparator winningBidComparator; + private Clock clock; private Timeout timeout; @@ -149,13 +151,12 @@ public void setUp() { given(cacheService.getEndpointHost()).willReturn("testHost"); given(cacheService.getEndpointPath()).willReturn("testPath"); given(cacheService.getCachedAssetURLTemplate()).willReturn("uuid="); - given(bidResponseReducer.removeRedundantBids(any())) - .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0)); given(storedRequestProcessor.videoStoredDataResult(any(), anyList(), anyList(), any())) .willReturn(Future.succeededFuture(VideoStoredDataResult.empty())); given(idGenerator.getType()).willReturn(IdGeneratorType.none); + winningBidComparator = new WinningBidComparator(); clock = Clock.fixed(Instant.ofEpochMilli(1000L), ZoneOffset.UTC); bidResponseCreator = new BidResponseCreator( @@ -164,7 +165,7 @@ public void setUp() { vastModifier, eventsService, storedRequestProcessor, - bidResponseReducer, + winningBidComparator, idGenerator, 0, clock, @@ -191,7 +192,7 @@ public void shouldPassOriginalTimeoutToCacheServiceIfCachingIsRequested() { givenCacheServiceResult(singletonMap(bid, CacheInfo.empty())); // when - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false); // then verify(cacheService).cacheBidsOpenrtb(anyList(), any(), any(), any()); @@ -240,7 +241,7 @@ public void shouldRequestCacheServiceWithExpectedArguments() { givenCacheServiceResult(singletonMap(bid1, CacheInfo.empty())); // when - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false); // then final BidInfo bidInfo1 = toBidInfo(bid1, imp1, "bidder1", banner); @@ -302,7 +303,7 @@ public void shouldRequestCacheServiceWithWinningBidsOnlyWhenWinningonlyIsTrue() givenCacheServiceResult(singletonMap(bid1, CacheInfo.empty())); // when - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false); // then final BidInfo bidInfo1 = toBidInfo(bid1, imp1, "bidder1", banner); @@ -343,7 +344,7 @@ public void shouldRequestCacheServiceWithVideoBidsToModify() { givenCacheServiceResult(singletonMap(bid1, CacheInfo.empty())); // when - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false); // then final BidInfo bidInfo1 = toBidInfo(bid1, imp1, "bidder1", video); @@ -374,7 +375,7 @@ public void shouldCallCacheServiceEvenRoundedCpmIsZero() { givenCacheServiceResult(singletonMap(bid1, CacheInfo.empty())); // when - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false); // then final BidInfo bidInfo1 = toBidInfo(bid1, imp1, "bidder1", banner); @@ -394,7 +395,7 @@ public void shouldSetExpectedConstantResponseFields() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, null, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, null, MULTI_BIDS, false).result(); // then final BidResponse responseWithExpectedFields = BidResponse.builder() @@ -418,7 +419,8 @@ public void shouldSetNbrValueTwoAndEmptySeatbidWhenIncomingBidResponsesAreEmpty( final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp())); // when - final BidResponse bidResponse = bidResponseCreator.create(emptyList(), auctionContext, null, false).result(); + final BidResponse bidResponse = bidResponseCreator.create(emptyList(), auctionContext, null, MULTI_BIDS, + false).result(); // then assertThat(bidResponse).returns(0, BidResponse::getNbr); @@ -436,7 +438,7 @@ public void shouldSetNbrValueTwoAndEmptySeatbidWhenIncomingBidResponsesDoNotCont // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, null, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, null, MULTI_BIDS, false).result(); // then assertThat(bidResponse).returns(0, BidResponse::getNbr); @@ -456,7 +458,7 @@ public void shouldSetNbrNullAndPopulateSeatbidWhenAtLeastOneBidIsPresent() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getNbr()).isNull(); @@ -477,7 +479,7 @@ public void shouldSkipBidderResponsesWhereSeatBidContainEmptyBids() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1); @@ -503,7 +505,7 @@ public void shouldOverrideBidIdWhenIdGeneratorIsUUID() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -556,7 +558,7 @@ public void shouldUseGeneratedBidIdForEventAndCacheWhenIdGeneratorIsUUIDAndEvent givenCacheServiceResult(singletonMap(bid, CacheInfo.of("id", null, null, null))); // when - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then final BidInfo expectedBidInfo = toBidInfo(bid, generatedBid, imp, bidder, banner); @@ -583,7 +585,7 @@ public void shouldSetExpectedResponseSeatBidAndBidFields() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -625,7 +627,7 @@ public void shouldNotWriteSkadnAttributeToBidderSection() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then final ObjectNode expectedBidExt = mapper.valueToTree(ExtPrebid.of( @@ -638,35 +640,6 @@ public void shouldNotWriteSkadnAttributeToBidderSection() { .containsExactly(expectedBidExt); } - @Test - public void shouldFilterBidByBidReducerResponse() { - // given - final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(givenImp("i1"), givenImp("i2"))); - - final Bid dealBid1Imp1 = Bid.builder().id("bidId1i1d").dealid("d1").price(BigDecimal.valueOf(4.98)).impid("i1") - .build(); - final Bid dealBid2Imp1 = Bid.builder().id("bidId2i1d").dealid("d2").price(BigDecimal.valueOf(5.00)).impid("i1") - .build(); - final BidderSeatBid seatBidWithDeals = givenSeatBid( - BidderBid.of(dealBid2Imp1, banner, null), - BidderBid.of(dealBid1Imp1, banner, null)); - - final List bidderResponses = singletonList(BidderResponse.of("bidder1", seatBidWithDeals, 100)); - - given(bidResponseReducer.removeRedundantBids(any())) - .willReturn(BidderResponse.of("bidder1", givenSeatBid(BidderBid.of(dealBid2Imp1, banner, null)), 100)); - - // when - final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); - - // then - assertThat(bidResponse.getSeatbid()) - .flatExtracting(SeatBid::getBid).hasSize(1) - .extracting(Bid::getId) - .containsOnly("bidId2i1d"); - } - @Test public void shouldAddTypeToNativeBidAdm() throws JsonProcessingException { // given @@ -705,7 +678,7 @@ public void shouldAddTypeToNativeBidAdm() throws JsonProcessingException { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -765,7 +738,7 @@ public void shouldReturnEmptyAssetIfImageTypeIsEmpty() throws JsonProcessingExce // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -818,7 +791,7 @@ public void shouldReturnEmptyAssetIfDataTypeIsEmpty() throws JsonProcessingExcep // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -852,7 +825,7 @@ public void shouldSetBidAdmToNullIfCacheIdIsPresentAndReturnCreativeBidsIsFalse( // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -881,7 +854,7 @@ public void shouldSetBidAdmToNullIfVideoCacheIdIsPresentAndReturnCreativeVideoBi // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -919,7 +892,7 @@ public void shouldModifyBidAdmWhenBidVideoAndVastModifierReturnValue() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -948,7 +921,7 @@ public void shouldSetBidExpWhenCacheIdIsMatched() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -977,7 +950,7 @@ public void shouldSetBidExpMaxTtlWhenCacheIdIsMatchedAndBothTtlIsSet() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -999,7 +972,7 @@ public void shouldTolerateMissingExtInSeatBidAndBid() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -1029,7 +1002,7 @@ public void shouldPopulateTargetingKeywords() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -1064,7 +1037,7 @@ public void shouldTruncateTargetingKeywordsByGlobalConfig() { vastModifier, eventsService, storedRequestProcessor, - bidResponseReducer, + winningBidComparator, idGenerator, 20, clock, @@ -1072,7 +1045,7 @@ public void shouldTruncateTargetingKeywordsByGlobalConfig() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -1106,7 +1079,7 @@ public void shouldTruncateTargetingKeywordsByAccountConfig() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -1149,7 +1122,7 @@ public void shouldTruncateTargetingKeywordsByRequestPassedValue() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -1164,6 +1137,126 @@ public void shouldTruncateTargetingKeywordsByRequestPassedValue() { tuple("hb_bidder_someVeryLo", "someVeryLongBidderName")); } + @Test + public void shouldReduceAndNotPopulateTargetingKeywordsForExtraBidsWhenCodePrefixIsNotDefined() { + // given + final AuctionContext auctionContext = givenAuctionContext(givenBidRequest( + identity(), + extBuilder -> extBuilder.targeting(givenTargeting()), + givenImp("i1"), givenImp("i2"))); + + final String bidder1 = "bidder1"; + final Map multiBidMap = new HashMap<>(); + multiBidMap.put(bidder1, ExtRequestPrebidMultiBid.of(bidder1, 3, null)); + + final Bid bidder1Bid1 = Bid.builder().id("bidder1Bid1").price(BigDecimal.valueOf(3.67)).impid("i1").build(); + final Bid bidder1Bid2 = Bid.builder().id("bidder1Bid2").price(BigDecimal.valueOf(4.98)).impid("i1").build(); + final Bid bidder1Bid3 = Bid.builder().id("bidder1Bid3").price(BigDecimal.valueOf(1.08)).impid("i1").build(); + final Bid bidder1Bid4 = Bid.builder().id("bidder1Bid4").price(BigDecimal.valueOf(11.8)).impid("i1").build(); + final Bid bidder1Bid5 = Bid.builder().id("bidder1Bid5").price(BigDecimal.valueOf(1.08)).impid("i2").build(); + + final List bidderResponses = singletonList( + BidderResponse.of(bidder1, + givenSeatBid( + BidderBid.of(bidder1Bid1, banner, null), // extra bid + BidderBid.of(bidder1Bid2, banner, null), // extra bid + BidderBid.of(bidder1Bid3, banner, null), // Will be removed by price + BidderBid.of(bidder1Bid4, banner, null), + BidderBid.of(bidder1Bid5, banner, null)), + 100)); + + // when + final BidResponse result = + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, multiBidMap, false).result(); + + // then + assertThat(result.getSeatbid()) + .flatExtracting(SeatBid::getBid).hasSize(4) + .extracting( + Bid::getId, + bid -> toTargetingByKey(bid, "hb_bidder"), + bid -> toTargetingByKey(bid, "hb_bidder_bidder1"), + BidResponseCreatorTest::getTargetingBidderCode) + .containsOnly( + tuple("bidder1Bid4", bidder1, bidder1, bidder1), + tuple("bidder1Bid2", null, null, null), + tuple("bidder1Bid1", null, null, null), + tuple("bidder1Bid5", bidder1, bidder1, null)); + + verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any()); + } + + @Test + public void shouldReduceAndPopulateTargetingKeywordsForExtraBidsWhenCodePrefixIsDefined() { + // given + final AuctionContext auctionContext = givenAuctionContext(givenBidRequest( + identity(), + extBuilder -> extBuilder.targeting(givenTargeting()), + givenImp("i1"), givenImp("i2"))); + + final String bidder1 = "bidder1"; + final String codePrefix = "bN"; + final Map multiBidMap = new HashMap<>(); + multiBidMap.put(bidder1, ExtRequestPrebidMultiBid.of(bidder1, 3, codePrefix)); + + final Bid bidder1Bid1 = Bid.builder().id("bidder1Bid1").price(BigDecimal.valueOf(3.67)).impid("i1").build(); + final Bid bidder1Bid2 = Bid.builder().id("bidder1Bid2").price(BigDecimal.valueOf(4.88)).impid("i1").build(); + final Bid bidder1Bid3 = Bid.builder().id("bidder1Bid3").price(BigDecimal.valueOf(1.08)).impid("i1").build(); + final Bid bidder1Bid4 = Bid.builder().id("bidder1Bid4").price(BigDecimal.valueOf(11.8)).impid("i1").build(); + final Bid bidder1Bid5 = Bid.builder().id("bidder1Bid5").price(BigDecimal.valueOf(1.08)).impid("i2").build(); + + final List bidderResponses = singletonList( + BidderResponse.of(bidder1, + givenSeatBid( + BidderBid.of(bidder1Bid1, banner, null), // extra bid + BidderBid.of(bidder1Bid2, banner, null), // extra bid + BidderBid.of(bidder1Bid3, banner, null), // Will be removed by price + BidderBid.of(bidder1Bid4, banner, null), + BidderBid.of(bidder1Bid5, banner, null)), + 100)); + + // when + final BidResponse result = + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, multiBidMap, false).result(); + + // then + final Map bidder1Bid4Targeting = new HashMap<>(); + bidder1Bid4Targeting.put("hb_pb", "5.00"); + bidder1Bid4Targeting.put("hb_pb_" + bidder1, "5.00"); + bidder1Bid4Targeting.put("hb_bidder_" + bidder1, bidder1); + bidder1Bid4Targeting.put("hb_bidder", bidder1); + final ObjectNode bidder1Bid4Ext = extWithTargeting(bidder1, bidder1Bid4Targeting); + final Bid expectedBidder1Bid4 = bidder1Bid4.toBuilder().ext(bidder1Bid4Ext).build(); + + final String bidderCodeForBidder1Bid2 = String.format("%s%s", codePrefix, 2); + final Map bidder1Bid2Targeting = new HashMap<>(); + bidder1Bid2Targeting.put("hb_bidder_" + bidderCodeForBidder1Bid2, bidderCodeForBidder1Bid2); + bidder1Bid2Targeting.put("hb_pb_" + bidderCodeForBidder1Bid2, "4.50"); + final ObjectNode bidder1Bid2Ext = extWithTargeting(bidderCodeForBidder1Bid2, bidder1Bid2Targeting); + final Bid expectedBidder1Bid2 = bidder1Bid2.toBuilder().ext(bidder1Bid2Ext).build(); + + final String bidderCodeForBidder1Bid1 = String.format("%s%s", codePrefix, 3); + final Map bidder1Bid1Targeting = new HashMap<>(); + bidder1Bid1Targeting.put("hb_bidder_" + bidderCodeForBidder1Bid1, bidderCodeForBidder1Bid1); + bidder1Bid1Targeting.put("hb_pb_" + bidderCodeForBidder1Bid1, "3.50"); + final ObjectNode bidder1Bid1Ext = extWithTargeting(bidderCodeForBidder1Bid1, bidder1Bid1Targeting); + final Bid expectedBidder1Bid1 = bidder1Bid1.toBuilder().ext(bidder1Bid1Ext).build(); + + final Map bidder1Bid5Targeting = new HashMap<>(); + bidder1Bid5Targeting.put("hb_pb", "1.00"); + bidder1Bid5Targeting.put("hb_pb_" + bidder1, "1.00"); + bidder1Bid5Targeting.put("hb_bidder_" + bidder1, bidder1); + bidder1Bid5Targeting.put("hb_bidder", bidder1); + final ObjectNode bidder1Bid5Ext = extWithTargeting(null, bidder1Bid5Targeting); + final Bid expectedBidder1Bid5 = bidder1Bid5.toBuilder().ext(bidder1Bid5Ext).build(); + + assertThat(result.getSeatbid()) + .flatExtracting(SeatBid::getBid).hasSize(4) + .containsOnly(expectedBidder1Bid4, expectedBidder1Bid2, expectedBidder1Bid1, expectedBidder1Bid5); + + verify(cacheService, never()).cacheBidsOpenrtb(anyList(), any(), any(), any()); + } + @Test public void shouldPopulateTargetingKeywordsForWinningBidsAndWinningBidsByBidder() { // given @@ -1177,14 +1270,15 @@ public void shouldPopulateTargetingKeywordsForWinningBidsAndWinningBidsByBidder( final Bid thirdBid = Bid.builder().id("bidId3").price(BigDecimal.valueOf(7.25)).impid("i2").build(); final List bidderResponses = asList( BidderResponse.of("bidder1", - givenSeatBid(BidderBid.of(firstBid, banner, null), + givenSeatBid( + BidderBid.of(firstBid, banner, null), BidderBid.of(secondBid, banner, null)), 100), BidderResponse.of("bidder2", givenSeatBid(BidderBid.of(thirdBid, banner, null)), 111)); // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -1233,7 +1327,7 @@ public void shouldPopulateTargetingKeywordsFromMediaTypePriceGranularities() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -1267,7 +1361,7 @@ public void shouldPopulateCacheIdHostPathAndUuidTargetingKeywords() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -1309,7 +1403,7 @@ public void shouldPopulateTargetingKeywordsWithAdditionalValuesFromRequest() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -1341,7 +1435,7 @@ public void shouldPopulateTargetingKeywordsIfBidWasCachedAndAdmWasRemoved() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // Check if you didn't lost any bids because of bid change in winningBids set // then @@ -1380,7 +1474,7 @@ public void shouldAddExtPrebidEventsIfEventsAreEnabledAndExtRequestPrebidEventPr // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -1423,7 +1517,7 @@ public void shouldAddExtPrebidEventsIfEventsAreEnabledAndAccountSupportEventsFor // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -1462,7 +1556,7 @@ public void shouldAddExtPrebidEventsIfEventsAreEnabledAndDefaultAccountAnalytics // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -1490,7 +1584,7 @@ public void shouldAddExtPrebidVideo() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -1516,7 +1610,7 @@ public void shouldNotAddExtPrebidEventsIfEventsAreNotEnabled() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -1543,7 +1637,7 @@ public void shouldNotAddExtPrebidEventsIfExtRequestPrebidEventsNull() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -1578,7 +1672,7 @@ public void shouldNotAddExtPrebidEventsIfAccountDoesNotSupportEventsForChannel() // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).hasSize(1) @@ -1609,7 +1703,7 @@ public void shouldReturnCacheEntityInExt() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()) @@ -1652,7 +1746,7 @@ public void shouldNotPopulateWinningBidTargetingIfIncludeWinnersFlagIsFalse() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).flatExtracting(SeatBid::getBid) @@ -1695,7 +1789,7 @@ public void shouldNotPopulateBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).flatExtracting(SeatBid::getBid) @@ -1730,7 +1824,7 @@ public void shouldNotPopulateCacheIdTargetingKeywordsIfBidCpmIsZero() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getSeatbid()).flatExtracting(SeatBid::getBid).hasSize(2) @@ -1762,7 +1856,7 @@ public void shouldNotCacheNonDealBidWithCpmIsZeroAndCacheDealBidWithZeroCpm() { givenCacheServiceResult(singletonMap(bid2, CacheInfo.of("cacheId2", null, null, null))); // when - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then final BidInfo bidInfo2 = toBidInfo(bid2, imp2, "bidder2", banner); @@ -1792,7 +1886,7 @@ public void shouldPopulateBidResponseExtension() throws JsonProcessingException // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false).result(); // then final ExtBidResponse responseExt = mapper.treeToValue(bidResponse.getExt(), ExtBidResponse.class); @@ -1839,7 +1933,7 @@ public void impToStoredVideoJsonShouldTolerateWhenStoredVideoFetchIsFailed() { // when final Future result = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false); // then verify(storedRequestProcessor).videoStoredDataResult(any(), eq(singletonList(imp)), anyList(), eq(timeout)); @@ -1891,7 +1985,7 @@ public void impToStoredVideoJsonShouldInjectStoredVideoWhenExtOptionsIsTrueAndVi // when final Future result = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false); // then verify(storedRequestProcessor).videoStoredDataResult(any(), eq(asList(imp1, imp3)), anyList(), @@ -1927,7 +2021,7 @@ public void impToStoredVideoJsonShouldAddErrorsWithPrebidBidderWhenStoredVideoRe // when final Future result = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false); // then verify(storedRequestProcessor).videoStoredDataResult(any(), eq(singletonList(imp1)), anyList(), eq(timeout)); @@ -1957,7 +2051,7 @@ public void shouldProcessRequestAndAddErrorAboutDeprecatedBidder() { // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false).result(); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false).result(); // then assertThat(bidResponse.getExt()).isEqualTo( @@ -1983,7 +2077,7 @@ public void shouldProcessRequestAndAddErrorFromAuctionContext() { // when final Future result = - bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, false); + bidResponseCreator.create(bidderResponses, auctionContext, CACHE_INFO, MULTI_BIDS, false); // then assertThat(result.result().getExt()).isEqualTo( @@ -2016,7 +2110,7 @@ public void shouldPopulateBidResponseDebugExtensionIfDebugIsEnabled() throws Jso // when final BidResponse bidResponse = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, true).result(); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, true).result(); // then final ExtBidResponse responseExt = mapper.treeToValue(bidResponse.getExt(), ExtBidResponse.class); @@ -2063,7 +2157,7 @@ public void shouldPassIntegrationToCacheServiceAndBidEvents() { // when final Future result = - bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, false); + bidResponseCreator.create(bidderResponses, auctionContext, cacheInfo, MULTI_BIDS, false); // then verify(cacheService).cacheBidsOpenrtb(anyList(), any(), any(), @@ -2184,6 +2278,22 @@ private static String toTargetingByKey(Bid bid, String targetingKey) { return targeting != null ? targeting.get(targetingKey) : null; } + private static String getTargetingBidderCode(Bid bid) { + return toExtPrebid(bid.getExt()).getPrebid().getTargetBidderCode(); + } + + private static ObjectNode extWithTargeting(String targetBidderCode, Map targeting) { + final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder() + .type(banner) + .targeting(targeting) + .targetBidderCode(targetBidderCode) + .build(); + + final ObjectNode ext = mapper.createObjectNode(); + ext.set("prebid", mapper.valueToTree(extBidPrebid)); + return ext; + } + @SafeVarargs private static List mutableList(T... values) { return Arrays.stream(values).collect(Collectors.toList()); diff --git a/src/test/java/org/prebid/server/auction/BidResponseReducerTest.java b/src/test/java/org/prebid/server/auction/BidResponseReducerTest.java deleted file mode 100644 index d20f0dbb716..00000000000 --- a/src/test/java/org/prebid/server/auction/BidResponseReducerTest.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.prebid.server.auction; - -import com.iab.openrtb.response.Bid; -import org.junit.Test; -import org.prebid.server.auction.model.BidderResponse; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.bidder.model.BidderSeatBid; -import org.prebid.server.proto.openrtb.ext.response.BidType; - -import java.math.BigDecimal; -import java.util.Arrays; - -import static org.assertj.core.api.Java6Assertions.assertThat; - -public class BidResponseReducerTest { - - private final BidResponseReducer bidResponseReducer = new BidResponseReducer(); - - @Test - public void removeRedundantBidsShouldReduceNonDealBidsByPriceDroppingNonDealsBids() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder1", - givenSeatBid( - givenBidderBid("bidId1", "impId1", "dealId1", 5.0f), // deal - givenBidderBid("bidId2", "impId1", "dealId2", 6.0f), // deal - givenBidderBid("bidId3", "impId1", null, 7.0f) // non deal - ), - 0); - - // when - final BidderResponse resultBidderResponse = bidResponseReducer.removeRedundantBids(bidderResponse); - - // then - assertThat(resultBidderResponse.getSeatBid().getBids()) - .extracting(BidderBid::getBid) - .extracting(Bid::getId) - .containsOnly("bidId2"); - } - - @Test - public void removeRedundantBidsShouldReduceNonDealBidsByPrice() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder1", - givenSeatBid( - givenBidderBid("bidId1", "impId1", null, 5.0f), // non deal - givenBidderBid("bidId2", "impId1", null, 6.0f), // non deal - givenBidderBid("bidId3", "impId1", null, 7.0f) // non deal - ), - 0); - - // when - final BidderResponse resultBidderResponse = bidResponseReducer.removeRedundantBids(bidderResponse); - - // then - assertThat(resultBidderResponse.getSeatBid().getBids()) - .extracting(BidderBid::getBid) - .extracting(Bid::getId) - .containsOnly("bidId3"); - } - - @Test - public void removeRedundantBidsShouldNotReduceBids() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder1", - givenSeatBid(givenBidderBid("bidId1", "impId1", null, 5.0f)), - 0); - - // when - final BidderResponse resultBidderResponse = bidResponseReducer.removeRedundantBids(bidderResponse); - - // then - assertThat(resultBidderResponse.getSeatBid().getBids()) - .extracting(BidderBid::getBid) - .extracting(Bid::getId) - .containsOnly("bidId1"); - } - - @Test - public void removeRedundantBidsShouldReduceAllTypesOfBidsForMultipleImps() { - // given - final BidderResponse bidderResponse = BidderResponse.of( - "bidder1", - givenSeatBid( - givenBidderBid("bidId3-1", "impId1", "dealId3", 5.0f), // deal - givenBidderBid("bidId4-1", "impId1", null, 5.0f), // non deal - givenBidderBid("bidId1-2", "impId2", "dealId4", 5.0f), // deal - givenBidderBid("bidId2-2", "impId2", "dealId5", 6.0f), // deal - givenBidderBid("bidId3-2", "impId2", null, 5.0f), // non deal - givenBidderBid("bidId1-3", "impId3", null, 5.0f), // non deal - givenBidderBid("bidId2-3", "impId3", null, 6.0f) // non deal - ), - 0); - - // when - final BidderResponse resultBidderResponse = bidResponseReducer.removeRedundantBids(bidderResponse); - - // then - assertThat(resultBidderResponse.getSeatBid().getBids()) - .extracting(BidderBid::getBid) - .extracting(Bid::getId) - .containsOnly("bidId3-1", "bidId2-2", "bidId2-3"); - } - - private static BidderBid givenBidderBid(String bidId, String impId, String dealId, float price) { - return BidderBid.of( - Bid.builder().id(bidId).impid(impId).dealid(dealId).price(BigDecimal.valueOf(price)).build(), - BidType.banner, "USD"); - } - - private static BidderSeatBid givenSeatBid(BidderBid... bidderBids) { - return BidderSeatBid.of(Arrays.asList(bidderBids), null, null); - } -} diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 29f8fd2d114..f32c7da2ae9 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -69,6 +69,7 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheBids; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidCacheVastxml; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidMultiBid; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchain; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSchainSchainNode; @@ -169,7 +170,7 @@ public class ExchangeServiceTest extends VertxTest { @SuppressWarnings("unchecked") @Before public void setUp() { - given(bidResponseCreator.create(anyList(), any(), any(), anyBoolean())) + given(bidResponseCreator.create(anyList(), any(), any(), any(), anyBoolean())) .willReturn(Future.succeededFuture(givenBidResponseWithBids(singletonList(givenBid(identity()))))); given(bidderCatalog.isValidName(anyString())).willReturn(true); @@ -668,7 +669,7 @@ public void shouldReturnSeparateSeatBidsForTheSameBidderIfBiddersAliasAndBidderW .auctiontimestamp(1000L) .build()))); - given(bidResponseCreator.create(anyList(), any(), any(), anyBoolean())) + given(bidResponseCreator.create(anyList(), any(), any(), any(), anyBoolean())) .willReturn(Future.succeededFuture(BidResponse.builder() .seatbid(asList( givenSeatBid(singletonList(givenBid(identity())), identity()), @@ -694,6 +695,7 @@ public void shouldCallBidResponseCreatorWithExpectedParams() { final Bid thirdBid = Bid.builder().id("bidId3").impid("impId1").price(BigDecimal.valueOf(7.89)).build(); givenBidder("bidder2", mock(Bidder.class), givenSeatBid(singletonList(givenBid(thirdBid)))); + final ExtRequestPrebidMultiBid multiBid1 = ExtRequestPrebidMultiBid.of("bidder1", 2, "bi"); final ExtRequestTargeting targeting = givenTargeting(true); final ObjectNode events = mapper.createObjectNode(); final BidRequest bidRequest = givenBidRequest(asList( @@ -704,6 +706,7 @@ public void shouldCallBidResponseCreatorWithExpectedParams() { .targeting(targeting) .auctiontimestamp(1000L) .events(events) + .multibid(singletonList(multiBid1)) .cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(53, true), ExtRequestPrebidCacheVastxml.of(34, true), true)) .build()))); @@ -724,8 +727,11 @@ public void shouldCallBidResponseCreatorWithExpectedParams() { .shouldCacheWinningBidsOnly(false) .build(); + final Map expectedMultiBidMap = singletonMap("bidder1", multiBid1); + final ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class); - verify(bidResponseCreator).create(captor.capture(), eq(auctionContext), eq(expectedCacheInfo), eq(false)); + verify(bidResponseCreator).create(captor.capture(), eq(auctionContext), eq(expectedCacheInfo), + eq(expectedMultiBidMap), eq(false)); assertThat(captor.getValue()).containsOnly( BidderResponse.of("bidder2", BidderSeatBid.of(singletonList( @@ -763,6 +769,7 @@ public void shouldCallBidResponseCreatorWithWinningOnlyTrueWhenIncludeBidderKeys anyList(), auctionContextArgumentCaptor.capture(), eq(BidRequestCacheInfo.builder().doCaching(true).shouldCacheWinningBidsOnly(true).build()), + eq(emptyMap()), eq(false)); assertThat(singletonList(auctionContextArgumentCaptor.getValue().getBidRequest())) @@ -801,6 +808,7 @@ public void shouldCallBidResponseCreatorWithWinningOnlyFalseWhenWinningOnlyIsNul anyList(), any(), eq(BidRequestCacheInfo.builder().build()), + eq(emptyMap()), anyBoolean()); } @@ -825,7 +833,7 @@ public void shouldCallBidResponseCreatorWithEnabledDebugTrueIfTestFlagIsTrue() { exchangeService.holdAuction(givenRequestContext(bidRequest)).result(); // then - verify(bidResponseCreator).create(anyList(), any(), any(), eq(true)); + verify(bidResponseCreator).create(anyList(), any(), any(), eq(emptyMap()), eq(true)); } @Test @@ -852,7 +860,7 @@ public void shouldCallBidResponseCreatorWithEnabledDebugTrueIfExtPrebidDebugIsOn exchangeService.holdAuction(givenRequestContext(bidRequest)).result(); // then - verify(bidResponseCreator).create(anyList(), any(), any(), eq(true)); + verify(bidResponseCreator).create(anyList(), any(), any(), any(), eq(true)); } @Test @@ -919,7 +927,7 @@ public void shouldTolerateResponseBidValidationErrors() { // then final ArgumentCaptor> bidderResponsesCaptor = ArgumentCaptor.forClass(List.class); - verify(bidResponseCreator).create(bidderResponsesCaptor.capture(), any(), any(), anyBoolean()); + verify(bidResponseCreator).create(bidderResponsesCaptor.capture(), any(), any(), any(), anyBoolean()); final List bidderResponses = bidderResponsesCaptor.getValue(); assertThat(bidderResponses) @@ -958,7 +966,7 @@ public void shouldTolerateResponseBidValidationWarnings() { // then final ArgumentCaptor> bidderResponsesCaptor = ArgumentCaptor.forClass(List.class); - verify(bidResponseCreator).create(bidderResponsesCaptor.capture(), any(), any(), anyBoolean()); + verify(bidResponseCreator).create(bidderResponsesCaptor.capture(), any(), any(), any(), anyBoolean()); final List bidderResponses = bidderResponsesCaptor.getValue(); assertThat(bidderResponses) @@ -1896,7 +1904,7 @@ public void shouldPassReducedGlobalTimeoutToConnectorAndOriginalToBidResponseCre final ArgumentCaptor timeoutCaptor = ArgumentCaptor.forClass(Timeout.class); verify(httpBidderRequester).requestBids(any(), any(), timeoutCaptor.capture(), anyBoolean()); assertThat(timeoutCaptor.getValue().remaining()).isEqualTo(400L); - verify(bidResponseCreator).create(anyList(), any(), any(), anyBoolean()); + verify(bidResponseCreator).create(anyList(), any(), any(), any(), anyBoolean()); } @Test @@ -1965,7 +1973,7 @@ public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() { // then final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); - verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean()); + verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any(), anyBoolean()); assertThat(argumentCaptor.getValue()).hasSize(1); @@ -1999,7 +2007,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor() // then final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); - verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean()); + verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any(), anyBoolean()); assertThat(argumentCaptor.getValue()).hasSize(1); @@ -2035,7 +2043,7 @@ public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForS // then final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); - verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean()); + verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any(), anyBoolean()); verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), any(), eq("CUR1")); verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), any(), eq("CUR2")); @@ -2076,7 +2084,7 @@ public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupported // then final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); - verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean()); + verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any(), anyBoolean()); verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("BAD"), eq("USD")); verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("BAD"), eq("CUR")); @@ -2117,7 +2125,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionAndAddErrorAboutMultipleCu // then final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); - verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean()); + verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any(), anyBoolean()); verify(currencyService).convertCurrency(eq(bidderPrice), eq(bidRequest), eq("CUR1"), eq("USD")); assertThat(argumentCaptor.getValue()).hasSize(1); @@ -2162,7 +2170,7 @@ public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() { // then final ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); - verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), anyBoolean()); + verify(bidResponseCreator).create(argumentCaptor.capture(), any(), any(), any(), anyBoolean()); verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("USD"), eq("EUR")); verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("USD"), eq("GBP")); verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD")); @@ -2484,12 +2492,12 @@ private static ExtRequestTargeting givenTargeting(boolean includebidderkeys) { } private void givenBidResponseCreator(List bids) { - given(bidResponseCreator.create(anyList(), any(), any(), anyBoolean())) + given(bidResponseCreator.create(anyList(), any(), any(), any(), anyBoolean())) .willReturn(Future.succeededFuture(givenBidResponseWithBids(bids))); } private void givenBidResponseCreator(Map> errors) { - given(bidResponseCreator.create(anyList(), any(), any(), anyBoolean())) + given(bidResponseCreator.create(anyList(), any(), any(), any(), anyBoolean())) .willReturn(Future.succeededFuture(givenBidResponseWithError(errors))); } diff --git a/src/test/java/org/prebid/server/auction/WinningBidComparatorTest.java b/src/test/java/org/prebid/server/auction/WinningBidComparatorTest.java new file mode 100644 index 00000000000..b0249dd3496 --- /dev/null +++ b/src/test/java/org/prebid/server/auction/WinningBidComparatorTest.java @@ -0,0 +1,81 @@ +package org.prebid.server.auction; + +import com.iab.openrtb.response.Bid; +import org.junit.Test; +import org.prebid.server.auction.model.BidInfo; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class WinningBidComparatorTest { + + private final WinningBidComparator target = new WinningBidComparator(); + + @Test + public void compareShouldReturnMoreThatZeroWhenFirstHasHigherPrice() { + // given + final BidInfo higherPriceBidInfo = givenBidInfo(5.0f); + final BidInfo loverPriceBidInfo = givenBidInfo(1.0f); + + // when + final int result = target.compare(higherPriceBidInfo, loverPriceBidInfo); + + // then + assertThat(result).isGreaterThan(0); + } + + @Test + public void compareShouldReturnLessThatZeroWhenFirstHasLowerPrice() { + // given + final BidInfo loverPriceBidInfo = givenBidInfo(1.0f); + final BidInfo higherPriceBidInfo = givenBidInfo(5.0f); + + // when + final int result = target.compare(loverPriceBidInfo, higherPriceBidInfo); + + // then + assertThat(result).isLessThan(0); + } + + @Test + public void compareShouldReturnZeroWhenPriceAreEqual() { + // given + final BidInfo bidInfo1 = givenBidInfo(5.0f); + final BidInfo bidInfo2 = givenBidInfo(5.0f); + + // when + final int result = target.compare(bidInfo1, bidInfo2); + + // then + assertThat(result).isEqualTo(0); + } + + @Test + public void sortShouldReturnExpectedSortedResult() { + // given + final BidInfo bidInfo1 = givenBidInfo(1.0f); + final BidInfo bidInfo2 = givenBidInfo(2.0f); + final BidInfo bidInfo3 = givenBidInfo(4.1f); + final BidInfo bidInfo4 = givenBidInfo(4.4f); + final BidInfo bidInfo5 = givenBidInfo(5.0f); + final BidInfo bidInfo6 = givenBidInfo(100.1f); + + final List bidInfos = Arrays.asList(bidInfo5, bidInfo3, bidInfo1, bidInfo2, bidInfo1, bidInfo4, + bidInfo6); + + // when + bidInfos.sort(target); + + // then + assertThat(bidInfos).containsOnly(bidInfo1, bidInfo1, bidInfo2, bidInfo3, bidInfo4, bidInfo5, bidInfo6); + } + + private static BidInfo givenBidInfo(float price) { + return BidInfo.builder() + .bid(Bid.builder().price(BigDecimal.valueOf(price)).build()) + .build(); + } +} diff --git a/src/test/java/org/prebid/server/it/ApplicationTest.java b/src/test/java/org/prebid/server/it/ApplicationTest.java index 2fb7bb08fcb..03a6f55f2de 100644 --- a/src/test/java/org/prebid/server/it/ApplicationTest.java +++ b/src/test/java/org/prebid/server/it/ApplicationTest.java @@ -134,6 +134,56 @@ public void openrtb2AuctionShouldRespondWithBidsFromRubiconAndAppnexus() throws JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbCacheDebugComparator()); } + @Test + public void openrtb2MultiBidAuctionShouldRespondWithBidsFromRubiconAndAppnexus() throws IOException, JSONException { + // given + // rubicon bid response for imp 1 + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange")) + .withQueryParam("tk_xint", equalTo("rp-pbs")) + .withBasicAuth("rubicon_user", "rubicon_password") + .withHeader("Content-Type", equalToIgnoreCase("application/json;charset=utf-8")) + .withHeader("Accept", equalTo("application/json")) + .withHeader("User-Agent", equalTo("prebid-server/1.0")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-request-1.json"))) + .willReturn(aResponse().withBody(jsonFrom( + "openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-response-1.json")))); + + // appnexus bid response for imp 1 + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/appnexus-exchange")) + .withRequestBody(equalToJson( + jsonFrom("openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-request-1.json"))) + .willReturn(aResponse().withBody(jsonFrom( + "openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-response-1.json")))); + + // pre-bid cache + WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/cache")) + .withRequestBody(equalToBidCacheRequest( + jsonFrom("openrtb2/rubicon_appnexus_multi_bid/test-cache-rubicon-appnexus-request.json"))) + .willReturn(aResponse() + .withTransformers("cache-response-transformer") + .withTransformerParameter("matcherName", + "openrtb2/rubicon_appnexus_multi_bid/test-cache-matcher-rubicon-appnexus.json") + )); + + // when + final Response response = given(SPEC) + .header("Referer", "http://www.example.com") + .header("User-Agent", "userAgent") + .header("Origin", "http://www.example.com") + // this uids cookie value stands for {"uids":{"rubicon":"J5VLCWQP-26-CWFT","adnxs":"12345"}} + .cookie("uids", "eyJ1aWRzIjp7InJ1Ymljb24iOiJKNVZMQ1dRUC0yNi1DV0ZUIiwiYWRueHMiOiIxMjM0NSJ9fQ==") + .body(jsonFrom("openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-request.json")) + .post("/openrtb2/auction"); + + // then + final String expectedAuctionResponse = openrtbAuctionResponseFrom( + "openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-response.json", + response, asList(RUBICON, APPNEXUS, APPNEXUS_ALIAS)); + + JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), openrtbCacheDebugComparator()); + } + @Test public void ampShouldReturnTargeting() throws IOException, JSONException { // given diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-request-1.json new file mode 100644 index 00000000000..30660e2a2dd --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-request-1.json @@ -0,0 +1,129 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "video": { + "mimes": [ + "mimes" + ], + "minduration": 20, + "maxduration": 60, + "protocols": [ + 1 + ], + "w": 300, + "h": 250, + "startdelay": 5, + "skipmin": 0, + "skipafter": 0, + "playbackmethod": [ + 1 + ] + }, + "tagid": "abc", + "bidfloor": 1.0, + "ext": { + "appnexus": { + "keywords": "foo=bar,foo=baz", + "traffic_source_code": "trafficSource" + } + } + } + ], + "site": { + "domain": "example.com", + "page": "http://www.example.com", + "publisher": { + "id": "5001" + }, + "ext": { + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "dnt": 2, + "ip": "80.215.195.0", + "pxratio": 4.2, + "language": "en", + "ext": { + "prebid": { + "interstitial": { + "minwidthperc": 50, + "minheightperc": 60 + } + } + } + }, + "user": { + "ext": { + "consent": "BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA" + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "source": { + "fd": 1, + "tid": "tid" + }, + "regs": { + "ext": { + "us_privacy": "1YNN" + } + }, + "ext": { + "prebid": { + "debug": 0, + "currency": { + "rates": { + "EUR": { + "USD": 1.2406 + }, + "USD": { + "EUR": 0.8110 + } + }, + "usepbsrates": false + }, + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + }, + "includewinners": true, + "includebidderkeys": true + }, + "cache": { + "bids": {}, + "vastxml": { + "ttlseconds": 120 + } + }, + "multibid": [ + { + "bidder": "rubicon", + "maxbids": 2, + "targetbiddercodeprefix": "rubN" + }, + { + "bidder": "appnexus", + "maxbids": 2 + } + ], + "auctiontimestamp": 1000, + "events" : { }, + "channel": { + "name": "web" + } + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-response-1.json new file mode 100644 index 00000000000..98c2116fea9 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-appnexus-bid-response-1.json @@ -0,0 +1,78 @@ +{ + "id": "tid", + "seatbid": [ + { + "seat": "958", + "bid": [ + { + "id": "7706636740145184841", + "impid": "impId1", + "price": 5.5, + "adid": "29681110", + "adm": "some-test-ad2", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 1, + "ranking_price": 0.000000 + } + } + }, + { + "id": "928185755156387460", + "impid": "impId1", + "price": 1.00, + "adid": "69595837", + "adm": "{\"assets\":[{\"id\":0,\"img\":{\"url\":\"http://vcdn.adnxs.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\":3000,\"h\":2250,\"ext\":{\"appnexus\":{\"prevent_crop\":0}}}},{\"id\":1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\":2,\"data\":{\"value\":\"Prebid.org\"}},{\"id\":3,\"data\":{\"value\":\"ThisisaPrebidNativeCreative.Therearemanylikeit,butthisoneismine.\"}}],\"link\":{\"url\":\"http://nym1-ib.adnxs.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://nym1-ib.adnxs.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"],\"jstracker\":\"\"}", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 1 + } + } + }, + { + "id": "222214214214", + "impid": "impId1", + "price": 2.00, + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "adm": "some-test-ad1", + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 1 + } + } + } + ] + } + ], + "bidid": "5778926625248726496", + "cur": "USD" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-request.json new file mode 100644 index 00000000000..869d6e0c668 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-request.json @@ -0,0 +1,162 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "video": { + "mimes": [ + "mimes" + ], + "minduration": 20, + "maxduration": 60, + "protocols": [ + 1 + ], + "w": 300, + "h": 250, + "startdelay": 5, + "skipmin": 0, + "skipafter": 0, + "playbackmethod": [ + 1 + ] + }, + "ext": { + "prebid": { + "bidder": { + "rubicon": { + "accountId": 2001, + "siteId": 3001, + "zoneId": 4001, + "inventory": { + "rating": [ + "5-star" + ], + "prodtype": [ + "tech" + ] + }, + "visitor": { + "ucat": [ + "new" + ], + "search": [ + "iphone" + ] + }, + "video": { + "size_id": 15, + "playerWidth": 780, + "playerHeight": "438", + "skip": 5, + "skipdelay": 1 + } + }, + "appnexus": { + "member": "103", + "inv_code": "abc", + "reserve": 1.0, + "position": "below", + "traffic_source_code": "trafficSource", + "keywords": [ + { + "key": "foo", + "value": [ + "bar", + "baz" + ] + } + ] + } + }, + "is_rewarded_inventory": 1 + } + } + } + ], + "device": { + "pxratio": 4.2, + "dnt": 2, + "ip": "80.215.195.122", + "language": "en", + "ifa": "ifaId", + "ext": { + "prebid": { + "interstitial": { + "minwidthperc": 50, + "minheightperc": 60 + } + } + } + }, + "site": { + "publisher": { + "id": "5001" + } + }, + "at": 1, + "tmax": 5000, + "cur": [ + "USD" + ], + "source": { + "fd": 1, + "tid": "tid" + }, + "ext": { + "prebid": { + "debug": 0, + "currency": { + "rates": { + "EUR": { + "USD": 1.2406 + }, + "USD": { + "EUR": 0.8110 + } + }, + "usepbsrates": false + }, + "events": {}, + "targeting": { + "pricegranularity": { + "precision": 2, + "ranges": [ + { + "max": 20, + "increment": 0.1 + } + ] + } + }, + "cache": { + "bids": {}, + "vastxml": { + "ttlseconds": 120 + } + }, + "multibid": [ + { + "bidder": "rubicon", + "maxbids": 2, + "targetbiddercodeprefix": "rubN" + }, + { + "bidder": "appnexus", + "maxbids": 2 + } + ], + "auctiontimestamp": 1000 + } + }, + "user": { + "ext": { + "consent": "BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA" + } + }, + "regs": { + "ext": { + "us_privacy": "1YNN" + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-response.json new file mode 100644 index 00000000000..ee6387c3c1c --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-auction-rubicon-appnexus-response.json @@ -0,0 +1,234 @@ +{ + "id": "tid", + "seatbid": [ + { + "bid": [ + { + "id": "21521324", + "impid": "impId1", + "price": 12.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "exp": 120, + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_size_rubicon": "300x250", + "hb_cache_id": "07a81993-a3f4-4582-89c1-44a6935b192b", + "hb_cache_path_rubicon": "{{ cache.path }}", + "hb_cache_host_rubicon": "{{ cache.host }}", + "hb_pb": "12.40", + "hb_pb_rubicon": "12.40", + "hb_cache_id_rubicon": "07a81993-a3f4-4582-89c1-44a6935b192b", + "hb_cache_path": "{{ cache.path }}", + "hb_uuid": "e15f6dcb-7b3b-4c32-be95-9b9d6f8f9320", + "hb_size": "300x250", + "hb_uuid_rubicon": "e15f6dcb-7b3b-4c32-be95-9b9d6f8f9320", + "hb_bidder": "rubicon", + "hb_bidder_rubicon": "rubicon", + "hb_cache_host": "{{ cache.host }}" + }, + "targetbiddercode": "rubicon", + "cache": { + "bids": { + "url": "{{ cache.resource_url }}07a81993-a3f4-4582-89c1-44a6935b192b", + "cacheId": "07a81993-a3f4-4582-89c1-44a6935b192b" + }, + "vastXml": { + "url": "{{ cache.resource_url }}e15f6dcb-7b3b-4c32-be95-9b9d6f8f9320", + "cacheId": "e15f6dcb-7b3b-4c32-be95-9b9d6f8f9320" + } + }, + "events": { + "win": "{{ event.url }}t=win&b=21521324&a=5001&ts=1000&bidder=rubicon&f=i&int=", + "imp": "{{ event.url }}t=imp&b=21521324&a=5001&ts=1000&bidder=rubicon&f=i&int=" + } + }, + "bidder": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + } + }, + { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "exp": 120, + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_pb_rubN2": "8.40", + "hb_uuid_rubN2": "d4f001de-4cc9-4857-9fad-70feff5c0dbc", + "hb_cache_host_rubN2": "{{ cache.host }}", + "hb_bidder_rubN2": "rubN2", + "hb_cache_path_rubN2": "{{ cache.path }}", + "hb_size_rubN2": "300x250", + "hb_cache_id_rubN2": "683fe79f-6df7-4971-ac70-820e0486992d" + }, + "targetbiddercode": "rubN2", + "cache": { + "bids": { + "url": "{{ cache.resource_url }}683fe79f-6df7-4971-ac70-820e0486992d", + "cacheId": "683fe79f-6df7-4971-ac70-820e0486992d" + }, + "vastXml": { + "url": "{{ cache.resource_url }}d4f001de-4cc9-4857-9fad-70feff5c0dbc", + "cacheId": "d4f001de-4cc9-4857-9fad-70feff5c0dbc" + } + }, + "events": { + "win": "{{ event.url }}t=win&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=", + "imp": "{{ event.url }}t=imp&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=" + } + }, + "bidder": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + } + } + ], + "seat": "rubicon", + "group": 0 + }, + { + "bid": [ + { + "id": "7706636740145184841", + "impid": "impId1", + "price": 5.5, + "adm": "some-test-ad2", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "exp": 120, + "ext": { + "prebid": { + "type": "video", + "targeting": { + "hb_pb_appnexus": "5.50", + "hb_uuid_appnexus": "5d7bfab8-69ea-416e-a5a1-a64db0bb218c", + "hb_cache_path_appnexus": "{{ cache.path }}", + "hb_bidder_appnexus": "appnexus", + "hb_cache_host_appnexus": "{{ cache.host }}", + "hb_cache_id_appnexus": "9ef753e9-e7b7-4309-b46a-65df40f75909" + }, + "targetbiddercode": "appnexus", + "cache": { + "bids": { + "url": "{{ cache.resource_url }}9ef753e9-e7b7-4309-b46a-65df40f75909", + "cacheId": "9ef753e9-e7b7-4309-b46a-65df40f75909" + }, + "vastXml": { + "url": "{{ cache.resource_url }}5d7bfab8-69ea-416e-a5a1-a64db0bb218c", + "cacheId": "5d7bfab8-69ea-416e-a5a1-a64db0bb218c" + } + }, + "events": { + "win": "{{ event.url }}t=win&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=", + "imp": "{{ event.url }}t=imp&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=" + } + }, + "bidder": { + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 1, + "ranking_price": 0.0 + } + } + } + }, + { + "id": "222214214214", + "impid": "impId1", + "price": 2.0, + "adm": "some-test-ad1", + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "cat": [ + "IAB20-3" + ], + "exp": 120, + "ext": { + "prebid": { + "type": "video", + "cache": { + "bids": { + "url": "{{ cache.resource_url }}c2da8624-f28a-4d01-bbd7-f348478c1185", + "cacheId": "c2da8624-f28a-4d01-bbd7-f348478c1185" + }, + "vastXml": { + "url": "{{ cache.resource_url }}d71b6640-811b-480b-9f34-e6858ec80e40", + "cacheId": "d71b6640-811b-480b-9f34-e6858ec80e40" + } + }, + "events": { + "win": "{{ event.url }}t=win&b=222214214214&a=5001&ts=1000&bidder=appnexus&f=i&int=", + "imp": "{{ event.url }}t=imp&b=222214214214&a=5001&ts=1000&bidder=appnexus&f=i&int=" + } + }, + "bidder": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 1 + } + } + } + } + ], + "seat": "appnexus", + "group": 0 + } + ], + "cur": "USD", + "ext": { + "responsetimemillis": { + "appnexus": "{{ appnexus.response_time_ms }}", + "rubicon": "{{ rubicon.response_time_ms }}", + "cache": "{{ cache.response_time_ms }}" + }, + "tmaxrequest": 5000, + "prebid": { + "auctiontimestamp": 1000 + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-matcher-rubicon-appnexus.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-matcher-rubicon-appnexus.json new file mode 100644 index 00000000000..1e754987294 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-matcher-rubicon-appnexus.json @@ -0,0 +1,10 @@ +{ + "880290288@8.43": "683fe79f-6df7-4971-ac70-820e0486992d", + "21521324@12.43": "07a81993-a3f4-4582-89c1-44a6935b192b", + "7706636740145184841@5.5": "9ef753e9-e7b7-4309-b46a-65df40f75909", + "222214214214@2": "c2da8624-f28a-4d01-bbd7-f348478c1185", + "": "e15f6dcb-7b3b-4c32-be95-9b9d6f8f9320", + "": "d4f001de-4cc9-4857-9fad-70feff5c0dbc", + "some-test-ad1": "d71b6640-811b-480b-9f34-e6858ec80e40", + "some-test-ad2": "5d7bfab8-69ea-416e-a5a1-a64db0bb218c" +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-rubicon-appnexus-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-rubicon-appnexus-request.json new file mode 100644 index 00000000000..e84906d0f2e --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-cache-rubicon-appnexus-request.json @@ -0,0 +1,129 @@ +{ + "puts": [ + { + "type": "json", + "value": { + "id": "21521324", + "impid": "impId1", + "price": 12.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + }, + "wurl": "http://localhost:8080/event?t=win&b=21521324&a=5001&ts=1000&bidder=rubicon&f=i&int=" + } + }, + { + "type": "json", + "value": { + "id": "7706636740145184841", + "impid": "impId1", + "price": 5.5, + "adm": "some-test-ad2", + "adid": "29681110", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110", + "cid": "958", + "crid": "29681110", + "ext": { + "appnexus": { + "brand_id": 1, + "auction_id": 8189378542222915032, + "bidder_id": 2, + "bid_ad_type": 1, + "ranking_price": 0.0 + } + }, + "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&ts=1000&bidder=appnexus&f=i&int=" + } + }, + { + "type": "json", + "value": { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + }, + "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&ts=1000&bidder=rubicon&f=i&int=" + } + }, + { + "type": "json", + "value": { + "id": "222214214214", + "impid": "impId1", + "price": 2, + "adm": "some-test-ad1", + "adid": "69595837", + "adomain": [ + "appnexus.com" + ], + "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837", + "cid": "958", + "crid": "69595837", + "cat": [ + "IAB20-3" + ], + "ext": { + "appnexus": { + "brand_id": 1, + "brand_category_id": 1, + "auction_id": 5607483846416358664, + "bidder_id": 2, + "bid_ad_type": 1 + } + }, + "wurl": "http://localhost:8080/event?t=win&b=222214214214&a=5001&ts=1000&bidder=appnexus&f=i&int=" + } + }, + { + "type": "xml", + "value": "some-test-ad1", + "expiry": 120 + }, + { + "type": "xml", + "value": "", + "expiry": 120 + }, + { + "type": "xml", + "value": "some-test-ad2", + "expiry": 120 + }, + { + "type": "xml", + "value": "", + "expiry": 120 + } + ] +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-request-1.json new file mode 100644 index 00000000000..f2355e70447 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-request-1.json @@ -0,0 +1,109 @@ +{ + "id": "tid", + "imp": [ + { + "id": "impId1", + "video": { + "mimes": [ + "mimes" + ], + "minduration": 20, + "maxduration": 60, + "protocols": [ + 1 + ], + "w": 300, + "h": 250, + "startdelay": 5, + "skipmin": 0, + "skipafter": 0, + "playbackmethod": [ + 1 + ], + "ext": { + "skip": 5, + "skipdelay": 1, + "videotype": "rewarded", + "rp": { + "size_id": 15 + } + } + }, + "ext": { + "rp": { + "zone_id": 4001, + "target": { + "rating": [ + "5-star" + ], + "prodtype": [ + "tech" + ], + "page": [ + "http://www.example.com" + ] + }, + "track": { + "mint": "", + "mint_version": "" + } + } + } + } + ], + "site": { + "domain": "example.com", + "page": "http://www.example.com", + "publisher": { + "ext": { + "rp": { + "account_id": 2001 + } + } + }, + "ext": { + "rp": { + "site_id": 3001 + }, + "amp": 0 + } + }, + "device": { + "ua": "userAgent", + "dnt": 2, + "ip": "80.215.195.0", + "pxratio": 4.2, + "language": "en", + "ext": { + "rp": { + "pixelratio": 4.2 + } + } + }, + "user": { + "ext": { + "consent": "BOEFEAyOEFEAyAHABDENAIgAAAB9vABAASA", + "rp": { + "target": { + "ucat": [ + "new" + ], + "search": [ + "iphone" + ] + } + } + } + }, + "at": 1, + "tmax": 5000, + "source": { + "fd": 1, + "tid": "tid" + }, + "regs": { + "ext": { + "us_privacy": "1YNN" + } + } +} diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-response-1.json new file mode 100644 index 00000000000..c4a18053670 --- /dev/null +++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon_appnexus_multi_bid/test-rubicon-bid-response-1.json @@ -0,0 +1,74 @@ +{ + "id": "bidResponseId1", + "seatbid": [ + { + "bid": [ + { + "id": "880290288", + "impid": "impId1", + "price": 8.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + }, + { + "id": "21521324", + "impid": "impId1", + "price": 12.43, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + }, + { + "id": "992342532", + "impid": "impId1", + "price": 2.00, + "adm": "", + "crid": "crid1", + "w": 300, + "h": 250, + "ext": { + "rp": { + "targeting": [ + { + "key": "rpfl_1001", + "values": [ + "2_tier0100" + ] + } + ] + } + } + } + ], + "seat": "seatId1", + "group": 0 + } + ] +}