diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 4ae191bcaad..9a1326b4837 100644 --- a/src/main/java/org/prebid/server/auction/ExchangeService.java +++ b/src/main/java/org/prebid/server/auction/ExchangeService.java @@ -5,6 +5,7 @@ import com.iab.openrtb.request.App; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Content; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Imp; @@ -97,6 +98,7 @@ import org.prebid.server.util.ListUtil; import org.prebid.server.util.PbsUtil; import org.prebid.server.util.StreamUtil; +import org.apache.commons.lang3.tuple.Pair; import java.math.BigDecimal; import java.time.Clock; @@ -502,10 +504,10 @@ private Future> makeAuctionParticipation( final ExtRequestPrebid prebid = requestExt == null ? null : requestExt.getPrebid(); final Map biddersToConfigs = getBiddersToConfigs(prebid); final Map> eidPermissions = getEidPermissions(prebid); - final Map bidderToUser = - prepareUsers(bidders, context, aliases, biddersToConfigs, eidPermissions); + final Map> bidderToUserAndDevice = + prepareUsersAndDevices(bidders, context, aliases, biddersToConfigs, eidPermissions); - return privacyEnforcementService.mask(context, bidderToUser, aliases) + return privacyEnforcementService.mask(context, bidderToUserAndDevice, aliases) .map(bidderToPrivacyResult -> getAuctionParticipation( bidderToPrivacyResult, bidRequest, @@ -557,7 +559,7 @@ private static List firstPartyDataBidders(ExtRequest requestExt) { return data == null ? null : data.getBidders(); } - private Map prepareUsers(List bidders, + private Map> prepareUsersAndDevices(List bidders, AuctionContext context, BidderAliases aliases, Map biddersToConfigs, @@ -566,7 +568,7 @@ private Map prepareUsers(List bidders, final BidRequest bidRequest = context.getBidRequest(); final List firstPartyDataBidders = firstPartyDataBidders(bidRequest.getExt()); - final Map bidderToUser = new HashMap<>(); + final Map> bidderToUserAndDevice = new HashMap<>(); for (String bidder : bidders) { final ExtBidderConfigOrtb fpdConfig = ObjectUtils.defaultIfNull(biddersToConfigs.get(bidder), biddersToConfigs.get(ALL_BIDDERS_CONFIG)); @@ -574,9 +576,11 @@ private Map prepareUsers(List bidders, .anyMatch(fpdBidder -> StringUtils.equalsIgnoreCase(fpdBidder, bidder)); final User preparedUser = prepareUser( bidder, context, aliases, useFirstPartyData, fpdConfig, eidPermissions); - bidderToUser.put(bidder, preparedUser); + final Device preparedDevice = prepareDevice( + bidRequest.getDevice(), fpdConfig, useFirstPartyData); + bidderToUserAndDevice.put(bidder, Pair.of(preparedUser, preparedDevice)); } - return bidderToUser; + return bidderToUserAndDevice; } private User prepareUser(String bidder, @@ -813,7 +817,6 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, final boolean isApp = preparedApp != null; final boolean isDooh = !isApp && preparedDooh != null; final boolean isSite = !isApp && !isDooh && preparedSite != null; - final List preparedImps = prepareImps( bidder, bidRequest, @@ -945,6 +948,13 @@ private App prepareApp(App app, ObjectNode fpdApp, boolean useFirstPartyData) { return useFirstPartyData ? fpdResolver.resolveApp(maskedApp, fpdApp) : maskedApp; } + private Device prepareDevice(Device device, ExtBidderConfigOrtb fpdConfig, boolean useFirstPartyData) { + if (fpdConfig == null) { + return device; + } + return useFirstPartyData ? fpdResolver.resolveDevice(device, fpdConfig.getDevice()) : device; + } + private static ExtApp maskExtApp(ExtApp appExt) { final ExtApp maskedExtApp = ExtApp.of(appExt.getPrebid(), null); return maskedExtApp.isEmpty() ? null : maskedExtApp; diff --git a/src/main/java/org/prebid/server/auction/FpdResolver.java b/src/main/java/org/prebid/server/auction/FpdResolver.java index f0ade099ece..5e91e38a2de 100644 --- a/src/main/java/org/prebid/server/auction/FpdResolver.java +++ b/src/main/java/org/prebid/server/auction/FpdResolver.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.App; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Site; import com.iab.openrtb.request.User; @@ -23,7 +24,8 @@ public class FpdResolver { private static final String BIDDERS = "bidders"; private static final String APP = "app"; private static final String DOOH = "dooh"; - private static final Set KNOWN_FPD_ATTRIBUTES = Set.of(USER, SITE, APP, DOOH, BIDDERS); + private static final String DEVICE = "device"; + private static final Set KNOWN_FPD_ATTRIBUTES = Set.of(USER, SITE, APP, DOOH, DEVICE, BIDDERS); private static final String CONTEXT = "context"; private static final String DATA = "data"; @@ -51,6 +53,10 @@ public Dooh resolveDooh(Dooh originDooh, ObjectNode fpdDooh) { return mergeFpd(originDooh, fpdDooh, Dooh.class); } + public Device resolveDevice(Device originDevice, ObjectNode fpdDevice) { + return mergeFpd(originDevice, fpdDevice, Device.class); + } + private T mergeFpd(T original, ObjectNode fpd, Class tClass) { if (fpd == null || fpd.isNull() || fpd.isMissingNode()) { return original; diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java index 9d18300197b..b14ff44444c 100644 --- a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java +++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java @@ -1,10 +1,12 @@ package org.prebid.server.auction.privacy.enforcement; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.User; import io.vertx.core.Future; import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; +import org.apache.commons.lang3.tuple.Pair; import java.util.List; import java.util.Map; @@ -22,14 +24,14 @@ public PrivacyEnforcementService(final List enforcements) { } public Future> mask(AuctionContext auctionContext, - Map bidderToUser, + Map> bidderToUserAndDevice, BidderAliases aliases) { - final List initialResults = bidderToUser.entrySet().stream() + final List initialResults = bidderToUserAndDevice.entrySet().stream() .map(entry -> BidderPrivacyResult.builder() .requestBidder(entry.getKey()) - .user(entry.getValue()) - .device(auctionContext.getBidRequest().getDevice()) + .user(entry.getValue().getLeft()) + .device(entry.getValue().getRight()) .build()) .toList(); diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java index d8dcf51e4aa..83203fb60cd 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java @@ -25,4 +25,9 @@ public class ExtBidderConfigOrtb { * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2.user */ ObjectNode user; + + /** + * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2.device + */ + ObjectNode device; } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index 25ee4657df9..c13ca6eb56f 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -322,10 +322,12 @@ public void setUp() { given(privacyEnforcementService.mask(any(), argThat(MapUtils::isNotEmpty), any())) .willAnswer(inv -> - Future.succeededFuture(((Map) inv.getArgument(1)).entrySet().stream() + Future.succeededFuture(((Map>) inv.getArgument(1)).entrySet() + .stream() .map(bidderAndUser -> BidderPrivacyResult.builder() .requestBidder(bidderAndUser.getKey()) - .user(bidderAndUser.getValue()) + .user(bidderAndUser.getValue().getLeft()) + .device(bidderAndUser.getValue().getRight()) .build()) .toList())); @@ -2691,12 +2693,12 @@ public void shouldUseConcreteOverGeneralSiteWithExtPrebidBidderConfigIgnoringCas final ObjectNode siteWithPage = mapper.valueToTree(Site.builder().page("testPage").build()); final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(siteWithPage, null, null, null)); + ExtBidderConfigOrtb.of(siteWithPage, null, null, null, null)); final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( singletonList("SoMeBiDdEr"), extBidderConfig); final ObjectNode siteWithDomain = mapper.valueToTree(Site.builder().domain("notUsed").build()); final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(siteWithDomain, null, null, null)); + ExtBidderConfigOrtb.of(siteWithDomain, null, null, null, null)); final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"), allExtBidderConfig); @@ -2738,12 +2740,12 @@ public void shouldUseConcreteOverGeneralDoohWithExtPrebidBidderConfig() { final ObjectNode doohWithVenueType = mapper.valueToTree(Dooh.builder().venuetype(List.of("venuetype")).build()); final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, null, doohWithVenueType, null)); + ExtBidderConfigOrtb.of(null, null, doohWithVenueType, null, null)); final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( singletonList("someBidder"), extBidderConfig); final ObjectNode doohWithDomain = mapper.valueToTree(Dooh.builder().domain("notUsed").build()); final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, null, doohWithDomain, null)); + ExtBidderConfigOrtb.of(null, null, doohWithDomain, null, null)); final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of( singletonList("*"), allExtBidderConfig); @@ -2787,7 +2789,7 @@ public void shouldUseConcreteOverGeneralAppWithExtPrebidBidderConfigIgnoringCase final Publisher publisherWithId = Publisher.builder().id("testId").build(); final ObjectNode appWithPublisherId = mapper.valueToTree(App.builder().publisher(publisherWithId).build()); final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, appWithPublisherId, null, null)); + ExtBidderConfigOrtb.of(null, appWithPublisherId, null, null, null)); final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( singletonList("SoMeBiDdEr"), extBidderConfig); @@ -2795,7 +2797,7 @@ public void shouldUseConcreteOverGeneralAppWithExtPrebidBidderConfigIgnoringCase final ObjectNode appWithUpdatedPublisher = mapper.valueToTree( App.builder().publisher(publisherWithIdAndDomain).build()); final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, appWithUpdatedPublisher, null, null)); + ExtBidderConfigOrtb.of(null, appWithUpdatedPublisher, null, null, null)); final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"), allExtBidderConfig); @@ -2834,13 +2836,13 @@ public void shouldUseConcreteOverGeneralUserWithExtPrebidBidderConfig() { givenBidder("someBidder", bidder, givenEmptySeatBid()); final ObjectNode bidderConfigUser = mapper.valueToTree(User.builder().id("userFromConfig").build()); final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, null, null, bidderConfigUser)); + ExtBidderConfigOrtb.of(null, null, null, bidderConfigUser, null)); final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( singletonList("SomMeBiDdEr"), extBidderConfig); final ObjectNode emptyUser = mapper.valueToTree(User.builder().build()); final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of( - ExtBidderConfigOrtb.of(null, null, null, emptyUser)); + ExtBidderConfigOrtb.of(null, null, null, emptyUser, null)); final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"), allExtBidderConfig); final User requestUser = User.builder().id("erased").buyeruid("testBuyerId").build(); @@ -2870,6 +2872,44 @@ public void shouldUseConcreteOverGeneralUserWithExtPrebidBidderConfig() { .containsOnly(mergedUser); } + @Test + public void shouldUseBidderSpecificDeviceDataInBidderRequest() { + // given + final Bidder bidder = mock(Bidder.class); + givenBidder("someBidder", bidder, givenEmptySeatBid()); + + final ObjectNode deviceWithMakeAndModel = mapper.valueToTree( + Device.builder().make("TestMake_001").model("TestModel_001").build()); + final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( + ExtBidderConfigOrtb.of(null, null, null, null, deviceWithMakeAndModel)); + final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( + singletonList("someBidder"), extBidderConfig); + final Device requestDevice = Device.builder().build(); + final ExtRequestPrebid extRequestPrebid = ExtRequestPrebid.builder() + .bidderconfig(singletonList(concreteFpdConfig)) + .build(); + final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("someBidder", 1)), + builder -> builder.device(requestDevice).ext(ExtRequest.of(extRequestPrebid))); + final Device mergedDevice = Device.builder() + .make("TestMake_001").model("TestModel_001").build(); + + given(fpdResolver.resolveDevice(any(), any())).willReturn(mergedDevice); + + // when + target.holdAuction(givenRequestContext(bidRequest)); + + // then + final ArgumentCaptor bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class); + verify(httpBidderRequester) + .requestBids(any(), bidderRequestCaptor.capture(), any(), any(), any(), any(), anyBoolean()); + final List capturedBidRequests = bidderRequestCaptor.getAllValues(); + + assertThat(capturedBidRequests) + .extracting(BidderRequest::getBidRequest) + .extracting(BidRequest::getDevice) + .containsOnly(mergedDevice); + } + @Test public void shouldAddBuyeridToUserFromRequest() { // given diff --git a/src/test/java/org/prebid/server/auction/FpdResolverTest.java b/src/test/java/org/prebid/server/auction/FpdResolverTest.java index d3b7530ab84..1e094a120d6 100644 --- a/src/test/java/org/prebid/server/auction/FpdResolverTest.java +++ b/src/test/java/org/prebid/server/auction/FpdResolverTest.java @@ -4,6 +4,7 @@ import com.iab.openrtb.request.App; import com.iab.openrtb.request.Content; import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Dooh; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Publisher; @@ -17,6 +18,9 @@ import org.prebid.server.json.JsonMerger; import org.prebid.server.proto.openrtb.ext.request.ExtApp; import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; +import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; import org.prebid.server.proto.openrtb.ext.request.ExtDooh; import org.prebid.server.proto.openrtb.ext.request.ExtSite; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -134,6 +138,80 @@ public void resolveUserShouldReturnCopyOfUserExtDataIfFPDUserExtDataIsMissing() assertThat(resultUser.getExt().getData().equals(originExtUserData)).isTrue(); // but the same by value } + @Test + public void resolveDeviceShouldOverrideFpdFieldsFromFpdDevice() { + // given + final Device originDevice = Device.builder() + .devicetype(1) + .make("original_make") + .model("original_model") + .os("original_os") + .osv("original_osv") + .hwv("original_hwv") + .language("original_language") + .h(1111) + .js(1) + .ip("original_ip") + .build(); + + final Device fpdDevice = Device.builder() + .devicetype(2) + .make("fpd_make") + .model("fpd_model") + .os("fpd_os") + .osv("fpd_osv") + .hwv("fpd_hwv") + .ip("new_ip") + .build(); + + // when + final Device resultDevice = target.resolveDevice(originDevice, mapper.valueToTree(fpdDevice)); + + // then + assertThat(resultDevice).isEqualTo(Device.builder() + .devicetype(2) + .make("fpd_make") + .model("fpd_model") + .os("fpd_os") + .osv("fpd_osv") + .hwv("fpd_hwv") + .language("original_language") + .h(1111) + .js(1) + .ip("new_ip") + .build()); + } + + @Test + public void resolveDeviceShouldReturnOriginDeviceIfFpdDeviceIsNull() { + assertThat(target.resolveDevice(Device.builder().make("test_make").build(), null)) + .isEqualTo(Device.builder().make("test_make").build()); + } + + @Test + public void resolveDeviceShouldReturnFpdDeviceIfOriginDeviceIsNull() { + assertThat(target.resolveDevice(null, mapper.valueToTree(Device.builder().model("test_model").build()))) + .isEqualTo(Device.builder().model("test_model").build()); + } + + @Test + public void resolveDeviceShouldNotChangeOriginExtDataIfFPDDoesNotHaveExt() { + // given + final Device originDevice = Device.builder() + .ext(ExtDevice.of(1, ExtDevicePrebid.of(ExtDeviceInt.of(10, 20)))) + .build(); + + final Device fpdDevice = Device.builder().build(); + + // when + final Device resultDevice = target.resolveDevice(originDevice, mapper.valueToTree(fpdDevice)); + + // then + assertThat(resultDevice).isEqualTo(Device.builder() + .ext(ExtDevice.of(1, ExtDevicePrebid.of(ExtDeviceInt.of(10, 20)))) + .build()); + } + @Test public void resolveAppShouldOverrideFpdFieldsFromFpdApp() { // given diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java index 906952bc9b3..187985e4149 100644 --- a/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java +++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java @@ -11,6 +11,7 @@ import org.prebid.server.auction.aliases.BidderAliases; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidderPrivacyResult; +import org.apache.commons.lang3.tuple.Pair; import java.util.List; import java.util.Map; @@ -55,10 +56,13 @@ public void maskShouldPassBidderPrivacyThroughAllEnforcements() { .build(); final User user = User.builder().id("originalUser").build(); - final Map bidderToUser = singletonMap("bidder", user); + final Device device = Device.builder().build(); + final Pair userAndDevice = Pair.of(user, device); + final Map> bidderToUserAndDevice = singletonMap("bidder", userAndDevice); // when - final Future> result = target.mask(auctionContext, bidderToUser, bidderAliases); + final Future> result = target.mask( + auctionContext, bidderToUserAndDevice, bidderAliases); // then assertThat(result) diff --git a/src/test/java/org/prebid/server/json/JsonMergerTest.java b/src/test/java/org/prebid/server/json/JsonMergerTest.java index 48957f12511..33551d129a8 100644 --- a/src/test/java/org/prebid/server/json/JsonMergerTest.java +++ b/src/test/java/org/prebid/server/json/JsonMergerTest.java @@ -34,6 +34,7 @@ public void mergeShouldReturnMergedObject() { siteWithPage, appWithPublisherId, doohWithVenueType, + null, null); final ObjectNode siteWithDomain = mapper.valueToTree(Site.builder().domain("testDomain").build()); @@ -41,8 +42,12 @@ public void mergeShouldReturnMergedObject() { final ObjectNode appWithUpdatedPublisher = mapper.valueToTree(App.builder() .publisher(publisherWithIdAndDomain).build()); final ObjectNode doohWithVenueTypeTax = mapper.valueToTree(Dooh.builder().venuetypetax(3).build()); - final ExtBidderConfigOrtb secondBidderConfigFpd = - ExtBidderConfigOrtb.of(siteWithDomain, appWithUpdatedPublisher, doohWithVenueTypeTax, null); + final ExtBidderConfigOrtb secondBidderConfigFpd = ExtBidderConfigOrtb.of( + siteWithDomain, + appWithUpdatedPublisher, + doohWithVenueTypeTax, + null, + null); // when final ExtBidderConfigOrtb result = target.merge( @@ -56,7 +61,12 @@ public void mergeShouldReturnMergedObject() { final ObjectNode mergedApp = mapper.valueToTree(App.builder().publisher(mergedPublisher).build()); final ObjectNode mergedDooh = mapper.valueToTree( Dooh.builder().venuetype(List.of("venuetype")).venuetypetax(3).build()); - final ExtBidderConfigOrtb mergedConfigFpd = ExtBidderConfigOrtb.of(mergedSite, mergedApp, mergedDooh, null); + final ExtBidderConfigOrtb mergedConfigFpd = ExtBidderConfigOrtb.of( + mergedSite, + mergedApp, + mergedDooh, + null, + null); assertThat(result).isEqualTo(mergedConfigFpd); }