diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java index 56cd8192501..d766b9112bc 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; @@ -768,12 +769,15 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, final App app = bidRequest.getApp(); final Site site = bidRequest.getSite(); final Dooh dooh = bidRequest.getDooh(); + final Device device = bidRequest.getDevice(); final ObjectNode fpdSite = fpdConfig != null ? fpdConfig.getSite() : null; final ObjectNode fpdApp = fpdConfig != null ? fpdConfig.getApp() : null; final ObjectNode fpdDooh = fpdConfig != null ? fpdConfig.getDooh() : null; + final ObjectNode fpdDevice = fpdConfig != null ? fpdConfig.getDevice() : null; final App preparedApp = prepareApp(app, fpdApp, useFirstPartyData); final Site preparedSite = prepareSite(site, fpdSite, useFirstPartyData); final Dooh preparedDooh = prepareDooh(dooh, fpdDooh, useFirstPartyData); + final Device preparedDevice = prepareDevice(device, fpdDevice, useFirstPartyData); final List distributionChannels = new ArrayList<>(); Optional.ofNullable(preparedApp).ifPresent(ignored -> distributionChannels.add("app")); @@ -800,6 +804,7 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, final boolean isApp = preparedApp != null; final boolean isDooh = !isApp && preparedDooh != null; final boolean isSite = !isApp && !isDooh && preparedSite != null; + final boolean preparedDeviceNotNull = preparedDevice != null; final List preparedImps = prepareImps( bidder, @@ -813,7 +818,7 @@ private BidRequest prepareBidRequest(BidderPrivacyResult bidderPrivacyResult, return bidRequest.toBuilder() // User was already prepared above .user(bidderPrivacyResult.getUser()) - .device(bidderPrivacyResult.getDevice()) + .device(preparedDeviceNotNull ? preparedDevice : bidderPrivacyResult.getDevice()) .imp(preparedImps) .app(isApp ? preparedApp : null) .dooh(isDooh ? preparedDooh : null) @@ -932,6 +937,10 @@ private App prepareApp(App app, ObjectNode fpdApp, boolean useFirstPartyData) { return useFirstPartyData ? fpdResolver.resolveApp(maskedApp, fpdApp) : maskedApp; } + private Device prepareDevice(Device device, ObjectNode fpdDevice, boolean useFirstPartyData) { + return useFirstPartyData ? fpdResolver.resolveDevice(device, fpdDevice) : 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/proto/openrtb/ext/request/ExtBidderConfigOrtb.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigOrtb.java index 59a6ecc08f7..b264417fe31 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 @@ -27,4 +27,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 2e8c9a5e2c1..4c7ce180934 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -2679,12 +2679,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); @@ -2726,12 +2726,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); @@ -2775,7 +2775,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); @@ -2783,7 +2783,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); @@ -2815,6 +2815,92 @@ public void shouldUseConcreteOverGeneralAppWithExtPrebidBidderConfigIgnoringCase .containsOnly(mergedApp); } + @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)); + // Bidder Config with specific device data + final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( + singletonList("someBidder"), extBidderConfig); + + final Device requestDevice = Device.builder().build(); + // Build ext request + 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 shouldOverrideWithBidderSpecificDeviceDataInBidderRequest() { + // - request Device is defined; + // - bidder FPD Device is defined + // - expect request Device to be overriden by bidder FPD Device in bidder request + + // given + final Bidder bidder = mock(Bidder.class); + givenBidder("someBidder", bidder, givenEmptySeatBid()); + + final ObjectNode deviceWithMakeAndModel = mapper.valueToTree( + Device.builder().make("TestMakeOver").model("TestModelOver").build()); + final ExtBidderConfig extBidderConfig = ExtBidderConfig.of( + ExtBidderConfigOrtb.of(null, null, null, null, deviceWithMakeAndModel)); + // Bidder Config with specific device data + final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of( + singletonList("someBidder"), extBidderConfig); + + final Device requestDevice = Device.builder().make("BaseMake").model("BaseModel").build(); + // Build ext request + 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("TestMakeOver").model("TestModelOver").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 shouldUseConcreteOverGeneralUserWithExtPrebidBidderConfig() { // given @@ -2822,13 +2908,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(); diff --git a/src/test/java/org/prebid/server/json/JsonMergerTest.java b/src/test/java/org/prebid/server/json/JsonMergerTest.java index 48957f12511..41142ceb89a 100644 --- a/src/test/java/org/prebid/server/json/JsonMergerTest.java +++ b/src/test/java/org/prebid/server/json/JsonMergerTest.java @@ -2,6 +2,7 @@ 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.Publisher; import com.iab.openrtb.request.Site; @@ -30,19 +31,25 @@ public void mergeShouldReturnMergedObject() { final Publisher publisherWithId = Publisher.builder().id("testId").build(); final ObjectNode appWithPublisherId = mapper.valueToTree(App.builder().publisher(publisherWithId).build()); final ObjectNode doohWithVenueType = mapper.valueToTree(Dooh.builder().venuetype(List.of("venuetype")).build()); + final ObjectNode deviceWithMakeAndModel = mapper.valueToTree(Device.builder() + .make("TestMake").model("TestModel").build()); final ExtBidderConfigOrtb firstBidderConfigFpd = ExtBidderConfigOrtb.of( siteWithPage, appWithPublisherId, doohWithVenueType, - null); + null, + deviceWithMakeAndModel); final ObjectNode siteWithDomain = mapper.valueToTree(Site.builder().domain("testDomain").build()); final Publisher publisherWithIdAndDomain = Publisher.builder().id("shouldNotBe").domain("domain").build(); final ObjectNode appWithUpdatedPublisher = mapper.valueToTree(App.builder() .publisher(publisherWithIdAndDomain).build()); final ObjectNode doohWithVenueTypeTax = mapper.valueToTree(Dooh.builder().venuetypetax(3).build()); + final ObjectNode deviceWithDeviceType = mapper.valueToTree(Device.builder().devicetype(6).build()); + final ExtBidderConfigOrtb secondBidderConfigFpd = - ExtBidderConfigOrtb.of(siteWithDomain, appWithUpdatedPublisher, doohWithVenueTypeTax, null); + ExtBidderConfigOrtb.of(siteWithDomain, appWithUpdatedPublisher, doohWithVenueTypeTax, + null, deviceWithDeviceType); // when final ExtBidderConfigOrtb result = target.merge( @@ -56,7 +63,10 @@ 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 ObjectNode mergedDevice = mapper.valueToTree(Device.builder().make("TestMake").model("TestModel") + .devicetype(6).build()); + final ExtBidderConfigOrtb mergedConfigFpd = ExtBidderConfigOrtb.of(mergedSite, mergedApp, mergedDooh, + null, mergedDevice); assertThat(result).isEqualTo(mergedConfigFpd); } @@ -65,24 +75,30 @@ public void mergeShouldReturnMergedObject() { public void mergeShouldReturnOriginalObjectWhenMergedObjectIsNull() { // given final Site site = Site.builder().build(); + final Device device = Device.builder().build(); // when final Site result = target.merge(site, null, Site.class); + final Device resultDevice = target.merge(device, null, Device.class); // then assertThat(result).isEqualTo(site); + assertThat(resultDevice).isEqualTo(device); } @Test public void mergeShouldReturnMergedObjectWhenOriginalObjectIsNull() { // given final Site site = Site.builder().build(); + final Device device = Device.builder().build(); // when final Site result = target.merge(null, site, Site.class); + final Device resultDevice = target.merge(null, device, Device.class); // then assertThat(result).isEqualTo(site); + assertThat(resultDevice).isEqualTo(device); } }