From 41c0e0171a2d66fe9e1cee2a67e5cec1e8459bff Mon Sep 17 00:00:00 2001 From: Oleksandr Zhevedenko <720803+Net-burst@users.noreply.github.com> Date: Fri, 26 Sep 2025 06:55:18 -0400 Subject: [PATCH 1/3] Documentation: Local dev request examples (#4189) --- sample/configs/localdev-config.yaml | 26 ++++++++++++ sample/profiles/README.md | 2 + sample/requests/localdev-test-request.http | 36 ++++++++++++++++ ...localdev-test-stored-auction-response.http | 36 ++++++++++++++++ .../localdev-test-stored-bidder-response.http | 42 +++++++++++++++++++ .../sample-stored-auction-response.json | 14 +++++++ .../stored/sample-stored-bidder-response.json | 17 ++++++++ 7 files changed, 173 insertions(+) create mode 100644 sample/configs/localdev-config.yaml create mode 100644 sample/profiles/README.md create mode 100644 sample/requests/localdev-test-request.http create mode 100644 sample/requests/localdev-test-stored-auction-response.http create mode 100644 sample/requests/localdev-test-stored-bidder-response.http create mode 100644 sample/stored/sample-stored-auction-response.json create mode 100644 sample/stored/sample-stored-bidder-response.json diff --git a/sample/configs/localdev-config.yaml b/sample/configs/localdev-config.yaml new file mode 100644 index 00000000000..c2991cdf3b6 --- /dev/null +++ b/sample/configs/localdev-config.yaml @@ -0,0 +1,26 @@ +status-response: "ok" +adapters: + generic: + enabled: true + endpoint: http://localhost +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + filesystem: + settings-filename: sample/configs/sample-app-settings.yaml + stored-requests-dir: sample/stored + stored-imps-dir: sample/stored + profiles-dir: sample/profiles + stored-responses-dir: sample/stored + categories-dir: +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 diff --git a/sample/profiles/README.md b/sample/profiles/README.md new file mode 100644 index 00000000000..ff7afdd219b --- /dev/null +++ b/sample/profiles/README.md @@ -0,0 +1,2 @@ +### This is a directory to store profiles for file-based configuration. +### Please put only valid OpenRTB JSON files here. diff --git a/sample/requests/localdev-test-request.http b/sample/requests/localdev-test-request.http new file mode 100644 index 00000000000..eb5da3d7b35 --- /dev/null +++ b/sample/requests/localdev-test-request.http @@ -0,0 +1,36 @@ +POST http://localhost:8080/openrtb2/auction +Content-Type: application/json + +{ + "id": "test-bid-request-id", + "site": { + "id": "test-site-id", + "name": "Test site", + "domain": "test.com", + "publisher": { + "id": "1001" + } + }, + "regs": { + "gdpr": 0 + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "prebid": { + "bidder": { + "generic": {} + } + } + } + } + ], + "test": 1 +} + +### diff --git a/sample/requests/localdev-test-stored-auction-response.http b/sample/requests/localdev-test-stored-auction-response.http new file mode 100644 index 00000000000..45509e61570 --- /dev/null +++ b/sample/requests/localdev-test-stored-auction-response.http @@ -0,0 +1,36 @@ +POST http://localhost:8080/openrtb2/auction +Content-Type: application/json + +{ + "id": "test-bid-request-id", + "site": { + "id": "test-site-id", + "name": "Test site", + "domain": "test.com", + "publisher": { + "id": "1001" + } + }, + "regs": { + "gdpr": 0 + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + } + } + ], + "ext": { + "prebid": { + "storedauctionresponse": { + "id": "sample-stored-auction-response" + } + } + }, + "test": 1 +} + +### diff --git a/sample/requests/localdev-test-stored-bidder-response.http b/sample/requests/localdev-test-stored-bidder-response.http new file mode 100644 index 00000000000..eec448271e5 --- /dev/null +++ b/sample/requests/localdev-test-stored-bidder-response.http @@ -0,0 +1,42 @@ +POST http://localhost:8080/openrtb2/auction +Content-Type: application/json + +{ + "id": "test-bid-request-id", + "site": { + "id": "test-site-id", + "name": "Test site", + "domain": "test.com", + "publisher": { + "id": "1001" + } + }, + "regs": { + "gdpr": 0 + }, + "imp": [ + { + "id": "test-imp-id", + "banner": { + "w": 300, + "h": 250 + }, + "ext": { + "prebid": { + "bidder": { + "generic": {} + }, + "storedbidresponse": [ + { + "bidder": "generic", + "id": "sample-stored-bidder-response" + } + ] + } + } + } + ], + "test": 1 +} + +### diff --git a/sample/stored/sample-stored-auction-response.json b/sample/stored/sample-stored-auction-response.json new file mode 100644 index 00000000000..34887d0fbb5 --- /dev/null +++ b/sample/stored/sample-stored-auction-response.json @@ -0,0 +1,14 @@ +{ + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "
Ad Content
", + "crid": "creative123", + "w": 300, + "h": 250 + } + ], + "seat": "stored-auction-response-seat" +} diff --git a/sample/stored/sample-stored-bidder-response.json b/sample/stored/sample-stored-bidder-response.json new file mode 100644 index 00000000000..e1dba2b29d1 --- /dev/null +++ b/sample/stored/sample-stored-bidder-response.json @@ -0,0 +1,17 @@ +{ + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "test-imp-id", + "price": 0.5, + "adm": "
Ad Content
", + "crid": "creative123", + "w": 300, + "h": 250 + } + ] + } + ] +} From 2383a149fc516b8674cafed9bc5134d14d2e5b86 Mon Sep 17 00:00:00 2001 From: Andrea Castello Date: Fri, 26 Sep 2025 12:57:26 +0200 Subject: [PATCH 2/3] wurfl-devicedetection module enhancements (#4182) --- .../devicedetection/v1/OrtbDeviceUpdater.java | 97 ++++++---- ...LDeviceDetectionRawAuctionRequestHook.java | 19 ++ .../v1/OrtbDeviceUpdaterTest.java | 169 +++++++++--------- ...iceDetectionRawAuctionRequestHookTest.java | 85 +++++++++ sample/configs/prebid-config-with-wurfl.yaml | 1 + 5 files changed, 249 insertions(+), 122 deletions(-) diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java index b9b624b4f5b..ec79b79049c 100644 --- a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java @@ -55,6 +55,7 @@ public AuctionRequestPayload apply(AuctionRequestPayload auctionRequestPayload) private Device update(Device ortbDevice) { final String make = tryUpdateField(ortbDevice.getMake(), this::getWurflMake); final String model = tryUpdateField(ortbDevice.getModel(), this::getWurflModel); + final String hwv = tryUpdateField(ortbDevice.getHwv(), this::getWurflModel); final Integer deviceType = tryUpdateField( Optional.ofNullable(ortbDevice.getDevicetype()) .filter(it -> it > 0) @@ -72,6 +73,7 @@ private Device update(Device ortbDevice) { .make(make) .model(model) .devicetype(deviceType) + .hwv(hwv) .os(os) .osv(osv) .h(h) @@ -103,49 +105,72 @@ private String getWurflModel() { } private Integer getWurflDeviceType() { - try { - if (wurflDevice.getVirtualCapabilityAsBool("is_mobile")) { - // if at least one of these capabilities is not defined, the mobile device type is undefined - final boolean isPhone = wurflDevice.getVirtualCapabilityAsBool("is_phone"); - final boolean isTablet = wurflDevice.getCapabilityAsBool("is_tablet"); - return isPhone || isTablet ? 1 : 6; - } - if (wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")) { - return 2; - } + if (getWurflIsOtt()) { + return 7; + } - if (wurflDevice.getCapabilityAsBool("is_connected_tv")) { - return 3; - } + if (getWurflIsConsole()) { + return 6; + } - if (wurflDevice.getCapabilityAsBool("is_phone")) { - return 4; - } + if ("out_of_home_device".equals(getWurflPhysicalFormFactor())) { + return 8; + } - if (wurflDevice.getCapabilityAsBool("is_tablet")) { - return 5; - } + final String formFactor = getWurflFormFactor(); + return switch (formFactor) { + case "Desktop" -> 2; + case "Smartphone", "Feature Phone" -> 4; + case "Tablet" -> 5; + case "Smart-TV" -> 3; + case "Other Non-Mobile" -> 6; + case "Other Mobile" -> 1; + default -> null; + }; + } - if (wurflDevice.getCapabilityAsBool("is_ott")) { - return 7; - } + private Boolean getWurflIsOtt() { + try { + return wurflDevice.getCapabilityAsBool("is_ott"); + } catch (CapabilityNotDefinedException e) { + logger.warn("Failed to get is_ott from WURFL device capabilities"); + return Boolean.FALSE; + } + } - final String physicalFormFactor = wurflDevice.getCapability("physical_form_factor"); - if (physicalFormFactor != null && physicalFormFactor.equals("out_of_home_device")) { - return 8; - } - } catch (CapabilityNotDefinedException | VirtualCapabilityNotDefinedException | NumberFormatException e) { - logger.warn("Failed to determine device type from WURFL device capabilities", e); + private String getWurflFormFactor() { + try { + return wurflDevice.getVirtualCapability("form_factor"); + } catch (VirtualCapabilityNotDefinedException e) { + logger.warn("Failed to get form_factor from WURFL device capabilities"); + return ""; + } + } + + private String getWurflPhysicalFormFactor() { + try { + return wurflDevice.getCapability("physical_form_factor"); + } catch (CapabilityNotDefinedException e) { + logger.warn("Failed to get physical_form_factor from WURFL device capabilities"); + return ""; + } + } + + private Boolean getWurflIsConsole() { + try { + return wurflDevice.getCapabilityAsBool("is_console"); + } catch (CapabilityNotDefinedException e) { + logger.warn("Failed to get is_console from WURFL device capabilities"); + return Boolean.FALSE; } - return null; } private String getWurflOs() { try { return wurflDevice.getVirtualCapability("advertised_device_os"); } catch (VirtualCapabilityNotDefinedException e) { - logger.warn("Failed to evaluate advertised device OS", e); + logger.warn("Failed to evaluate advertised device OS"); return null; } } @@ -154,7 +179,7 @@ private String getWurflOsv() { try { return wurflDevice.getVirtualCapability("advertised_device_os_version"); } catch (VirtualCapabilityNotDefinedException e) { - logger.warn("Failed to evaluate advertised device OS version", e); + logger.warn("Failed to evaluate advertised device OS version"); } return null; } @@ -163,7 +188,7 @@ private Integer getWurflH() { try { return wurflDevice.getCapabilityAsInt("resolution_height"); } catch (NumberFormatException e) { - logger.warn("Failed to get resolution height from WURFL device capabilities", e); + logger.warn("Failed to get resolution height from WURFL device capabilities"); return null; } } @@ -172,7 +197,7 @@ private Integer getWurflW() { try { return wurflDevice.getCapabilityAsInt("resolution_width"); } catch (NumberFormatException e) { - logger.warn("Failed to get resolution width from WURFL device capabilities", e); + logger.warn("Failed to get resolution width from WURFL device capabilities"); return null; } } @@ -181,7 +206,7 @@ private Integer getWurflPpi() { try { return wurflDevice.getVirtualCapabilityAsInt("pixel_density"); } catch (VirtualCapabilityNotDefinedException e) { - logger.warn("Failed to get pixel density from WURFL device capabilities", e); + logger.warn("Failed to get pixel density from WURFL device capabilities"); return null; } } @@ -193,7 +218,7 @@ private BigDecimal getWurflPxRatio() { ? new BigDecimal(densityAsString) : null; } catch (CapabilityNotDefinedException | NumberFormatException e) { - logger.warn("Failed to get pixel ratio from WURFL device capabilities", e); + logger.warn("Failed to get pixel ratio from WURFL device capabilities"); return null; } } @@ -202,7 +227,7 @@ private Integer getWurflJs() { try { return wurflDevice.getCapabilityAsBool("ajax_support_javascript") ? 1 : 0; } catch (CapabilityNotDefinedException | NumberFormatException e) { - logger.warn("Failed to get JS support from WURFL device capabilities", e); + logger.warn("Failed to get JS support from WURFL device capabilities"); return null; } } diff --git a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java index 337343f15d4..5a92e051f0c 100644 --- a/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java +++ b/extra/modules/wurfl-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; import com.iab.openrtb.request.Device; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; import com.iab.openrtb.request.BidRequest; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; @@ -31,6 +32,7 @@ public class WURFLDeviceDetectionRawAuctionRequestHook implements RawAuctionRequ private static final Logger logger = LoggerFactory.getLogger(WURFLDeviceDetectionRawAuctionRequestHook.class); public static final String CODE = "wurfl-devicedetection-raw-auction-request"; + private static final String WURFL_PROPERTY = "wurfl"; private final WURFLService wurflService; private final Set allowedPublisherIDs; @@ -62,6 +64,11 @@ public Future> call(AuctionRequestPayloa return noActionResult(); } + if (isDeviceAlreadyEnriched(device)) { + logger.info("Device is already enriched, returning original bid request"); + return noActionResult(); + } + final Map requestHeaders = invocationContext.moduleContext() instanceof AuctionRequestHeadersContext moduleContext ? moduleContext.getHeaders() @@ -87,6 +94,18 @@ public Future> call(AuctionRequestPayloa .build()); } + private boolean isDeviceAlreadyEnriched(Device device) { + final ExtDevice extDevice = device.getExt(); + if (extDevice != null && extDevice.containsProperty(WURFL_PROPERTY)) { + return true; + } + + // Check if other some of the other Device data are already set + final Integer deviceType = device.getDevicetype(); + final String hwv = device.getHwv(); + return deviceType != null && deviceType > 0 && StringUtils.isNotEmpty(hwv); + } + private boolean shouldEnrichDevice(AuctionInvocationContext invocationContext) { return CollectionUtils.isEmpty(allowedPublisherIDs) || isAccountValid(invocationContext.auctionContext()); } diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java index 54e7464619d..43300b0ff92 100644 --- a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.Device; +import com.scientiamobile.wurfl.core.exc.VirtualCapabilityNotDefinedException; import org.mockito.Mock; import org.prebid.server.proto.openrtb.ext.request.ExtDevice; import org.junit.jupiter.api.BeforeEach; @@ -19,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class OrtbDeviceUpdaterTest { @@ -46,13 +48,10 @@ void setUp() { @Test public void updateShouldUpdateDeviceMakeWhenOriginalIsEmpty() { // given - given(wurflDevice.getCapability("brand_name")).willReturn("Apple"); - given(wurflDevice.getCapability("model_name")).willReturn("iPhone"); - given(wurflDevice.getCapabilityAsBool("ajax_support_javascript")).willReturn(true); - given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(true); - given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(true); + given(wurflDevice.getCapability("brand_name")).willReturn("TestPhone"); given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false); given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); @@ -62,22 +61,18 @@ public void updateShouldUpdateDeviceMakeWhenOriginalIsEmpty() { // then final Device resultDevice = result.bidRequest().getDevice(); - assertThat(resultDevice.getMake()).isEqualTo("Apple"); - assertThat(resultDevice.getDevicetype()).isEqualTo(1); + assertThat(resultDevice.getMake()).isEqualTo("TestPhone"); + assertThat(resultDevice.getDevicetype()).isEqualTo(4); } @Test public void updateShouldNotUpdateDeviceMakeWhenOriginalExists() { // given - final Device device = Device.builder().make("Samsung").build(); + final Device device = Device.builder().make("Samphone").model("youPhone").build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); - given(wurflDevice.getCapability("brand_name")).willReturn("Apple"); - given(wurflDevice.getCapability("model_name")).willReturn("iPhone"); - given(wurflDevice.getCapability("ajax_support_javascript")).willReturn("true"); - given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true"); - given(wurflDevice.getVirtualCapability("is_phone")).willReturn("true"); - given(wurflDevice.getCapability("is_tablet")).willReturn("false"); - given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false"); + given(wurflDevice.getCapability("brand_name")).willReturn("TestPhone"); + given(wurflDevice.getCapability("model_name")).willReturn("youPhone"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); given(payload.bidRequest()).willReturn(bidRequest); @@ -86,21 +81,18 @@ public void updateShouldNotUpdateDeviceMakeWhenOriginalExists() { // then final Device resultDevice = result.bidRequest().getDevice(); - assertThat(resultDevice.getMake()).isEqualTo("Samsung"); + assertThat(resultDevice.getMake()).isEqualTo("Samphone"); + assertThat(resultDevice.getModel()).isEqualTo("youPhone"); } @Test public void updateShouldNotUpdateDeviceMakeWhenOriginalBigIntegerExists() { // given - final Device device = Device.builder().make("Apple").pxratio(new BigDecimal("1.0")).build(); + final Device device = Device.builder().make("TestPhone").pxratio(new BigDecimal("1.0")).build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); - given(wurflDevice.getCapability("brand_name")).willReturn("Apple"); - given(wurflDevice.getCapability("model_name")).willReturn("iPhone"); - given(wurflDevice.getCapability("ajax_support_javascript")).willReturn("true"); - given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true"); - given(wurflDevice.getVirtualCapability("is_phone")).willReturn("true"); - given(wurflDevice.getCapability("is_tablet")).willReturn("false"); - given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false"); + given(wurflDevice.getCapability("brand_name")).willReturn("TestPhone"); + given(wurflDevice.getCapability("density_class")).willReturn("2.2"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); given(payload.bidRequest()).willReturn(bidRequest); @@ -109,7 +101,7 @@ public void updateShouldNotUpdateDeviceMakeWhenOriginalBigIntegerExists() { // then final Device resultDevice = result.bidRequest().getDevice(); - assertThat(resultDevice.getMake()).isEqualTo("Apple"); + assertThat(resultDevice.getMake()).isEqualTo("TestPhone"); assertThat(resultDevice.getPxratio()).isEqualTo("1.0"); } @@ -118,13 +110,8 @@ public void updateShouldUpdateDeviceModelWhenOriginalIsEmpty() { // given final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); - given(wurflDevice.getCapability("brand_name")).willReturn("Apple"); - given(wurflDevice.getCapability("model_name")).willReturn("iPhone"); - given(wurflDevice.getCapability("ajax_support_javascript")).willReturn("true"); - given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true"); - given(wurflDevice.getVirtualCapability("is_phone")).willReturn("true"); - given(wurflDevice.getCapability("is_tablet")).willReturn("false"); - given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false"); + given(wurflDevice.getCapability("model_name")).willReturn("youPhone"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); given(payload.bidRequest()).willReturn(bidRequest); @@ -133,7 +120,7 @@ public void updateShouldUpdateDeviceModelWhenOriginalIsEmpty() { // then final Device resultDevice = result.bidRequest().getDevice(); - assertThat(resultDevice.getModel()).isEqualTo("iPhone"); + assertThat(resultDevice.getModel()).isEqualTo("youPhone"); } @Test @@ -141,10 +128,8 @@ public void updateShouldUpdateDeviceOsWhenOriginalIsEmpty() { // given final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); - given(wurflDevice.getCapability("brand_name")).willReturn("Apple"); - given(wurflDevice.getCapability("model_name")).willReturn("iPhone"); - given(wurflDevice.getVirtualCapability("advertised_device_os")).willReturn("iOS"); - given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false); + given(wurflDevice.getVirtualCapability("advertised_device_os")).willReturn("testOS"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); given(payload.bidRequest()).willReturn(bidRequest); @@ -153,7 +138,7 @@ public void updateShouldUpdateDeviceOsWhenOriginalIsEmpty() { // then final Device resultDevice = result.bidRequest().getDevice(); - assertThat(resultDevice.getOs()).isEqualTo("iOS"); + assertThat(resultDevice.getOs()).isEqualTo("testOS"); } @Test @@ -161,13 +146,7 @@ public void updateShouldUpdateResolutionWhenOriginalIsEmpty() { // given final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); - given(wurflDevice.getCapability("brand_name")).willReturn("Apple"); - given(wurflDevice.getCapability("model_name")).willReturn("iPhone"); - given(wurflDevice.getCapabilityAsBool("ajax_support_javascript")).willReturn(true); - given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(true); - given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(true); - given(wurflDevice.getVirtualCapabilityAsBool("is_tablet")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); given(wurflDevice.getCapabilityAsInt("resolution_width")).willReturn(3200); given(wurflDevice.getCapabilityAsInt("resolution_height")).willReturn(1440); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); @@ -188,6 +167,7 @@ public void updateShouldHandleJavascriptSupport() { final BidRequest bidRequest = BidRequest.builder().device(device).build(); given(wurflDevice.getCapabilityAsBool("ajax_support_javascript")).willReturn(true); given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(true); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); given(payload.bidRequest()).willReturn(bidRequest); @@ -205,10 +185,6 @@ public void updateShouldHandleOttDeviceType() { final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); given(wurflDevice.getCapabilityAsBool("is_ott")).willReturn(true); - given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false); - given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); given(payload.bidRequest()).willReturn(bidRequest); // when @@ -225,8 +201,6 @@ public void updateShouldHandleOutOfHomeDeviceType() { final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); given(wurflDevice.getCapability("physical_form_factor")).willReturn("out_of_home_device"); - given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false); - given(wurflDevice.getCapabilityAsBool("is_wireless_device")).willReturn(false); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); given(payload.bidRequest()).willReturn(bidRequest); // when @@ -242,12 +216,7 @@ public void updateShouldReturnDeviceOtherMobileWhenMobileIsNotPhoneOrTablet() { // given final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); - given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(true); - given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false); - given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false); - given(wurflDevice.getVirtualCapability("advertised_device_os")).willReturn("TestOs"); - given(wurflDevice.getVirtualCapability("advertised_device_os_version")).willReturn("1.0"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Other Non-Mobile"); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); given(payload.bidRequest()).willReturn(bidRequest); @@ -259,12 +228,28 @@ public void updateShouldReturnDeviceOtherMobileWhenMobileIsNotPhoneOrTablet() { } @Test - public void updateShouldReturnNullWhenMobileTypeIsUnknown() { + public void updateShouldReturnNullDeviceTypeWhenMobileTypeIsUnknown() { + // given + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Other non-Mobile"); + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + // when + given(payload.bidRequest()).willReturn(bidRequest); + final AuctionRequestPayload result = target.apply(payload); + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isNull(); + } + + @Test + public void updateShouldReturnNullDeviceTypeWhenFormFactorIsMissing() { // given - given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("false"); + given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true"); given(wurflDevice.getVirtualCapability("is_phone")).willReturn("false"); given(wurflDevice.getCapability("is_tablet")).willReturn("false"); - given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false"); + when(wurflDevice.getVirtualCapability("form_factor")) + .thenThrow(new VirtualCapabilityNotDefinedException("form_factor")); final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); @@ -276,17 +261,27 @@ public void updateShouldReturnNullWhenMobileTypeIsUnknown() { assertThat(resultDevice.getDevicetype()).isNull(); } + @Test + public void updateShouldReturnGenericMobileTabletDeviceTypeWhenFormFactorIsOtherMobile() { + // given + given(wurflDevice.getCapability("is_tablet")).willReturn("false"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Other Mobile"); + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + // when + given(payload.bidRequest()).willReturn(bidRequest); + final AuctionRequestPayload result = target.apply(payload); + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(1); + } + @Test public void updateShouldHandlePersonalComputerDeviceType() { // given final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); - given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false); - given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(true); - given(wurflDevice.getVirtualCapability("advertised_device_os")).willReturn("Windows"); - given(wurflDevice.getVirtualCapability("advertised_device_os_version")).willReturn("10"); given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Desktop"); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); // when @@ -303,14 +298,7 @@ public void updateShouldHandleConnectedTvDeviceType() { // given final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); - given(wurflDevice.getCapabilityAsBool("is_connected_tv")).willReturn(true); - given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_tablet")).willReturn(false); - given(wurflDevice.getVirtualCapability("advertised_device_os")).willReturn("WebOS"); - given(wurflDevice.getVirtualCapability("advertised_device_os_version")).willReturn("4"); - given(wurflDevice.getCapabilityAsBool("is_connected_tv")).willReturn(true); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smart-TV"); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); // when given(payload.bidRequest()).willReturn(bidRequest); @@ -326,10 +314,6 @@ public void updateShouldNotUpdateDeviceTypeWhenSet() { final Device device = Device.builder() .devicetype(3) .build(); - given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(true); - given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_tablet")).willReturn(false); // device type 2 final BidRequest bidRequest = BidRequest.builder().device(device).build(); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); // when @@ -343,12 +327,7 @@ public void updateShouldNotUpdateDeviceTypeWhenSet() { @Test public void updateShouldHandleTabletDeviceType() { // given - given(wurflDevice.getCapabilityAsBool("is_tablet")).willReturn(true); - given(wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_mobile")).willReturn(false); - given(wurflDevice.getVirtualCapabilityAsBool("is_phone")).willReturn(false); - given(wurflDevice.getCapability("brand_name")).willReturn("Samsung"); - given(wurflDevice.getCapability("model_name")).willReturn("Galaxy Tab S9+"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Tablet"); final Device device = Device.builder().build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); @@ -362,6 +341,23 @@ public void updateShouldHandleTabletDeviceType() { assertThat(resultDevice.getDevicetype()).isEqualTo(5); } + @Test + public void updateShouldHandlePhoneDeviceType() { + // given + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); + + final Device device = Device.builder().build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); + given(payload.bidRequest()).willReturn(bidRequest); + // when + final AuctionRequestPayload result = target.apply(payload); + + // then + final Device resultDevice = result.bidRequest().getDevice(); + assertThat(resultDevice.getDevicetype()).isEqualTo(4); + } + @Test public void updateShouldAddWurflPropertyToExtIfMissingAndPreserveExistingProperties() { // given @@ -371,13 +367,14 @@ public void updateShouldAddWurflPropertyToExtIfMissingAndPreserveExistingPropert .ext(existingExt) .build(); final BidRequest bidRequest = BidRequest.builder().device(device).build(); - given(wurflDevice.getCapability("brand_name")).willReturn("Apple"); - given(wurflDevice.getCapability("model_name")).willReturn("iPhone"); + given(wurflDevice.getCapability("brand_name")).willReturn("TestPhone"); + given(wurflDevice.getCapability("model_name")).willReturn("youPhone"); given(wurflDevice.getCapability("ajax_support_javascript")).willReturn("true"); given(wurflDevice.getVirtualCapability("is_mobile")).willReturn("true"); given(wurflDevice.getVirtualCapability("is_phone")).willReturn("true"); given(wurflDevice.getCapability("is_tablet")).willReturn("false"); given(wurflDevice.getVirtualCapability("is_full_desktop")).willReturn("false"); + given(wurflDevice.getVirtualCapability("form_factor")).willReturn("Smartphone"); final Set staticCaps = Set.of("brand_name"); final Set virtualCaps = Set.of("advertised_device_os"); final OrtbDeviceUpdater target = new OrtbDeviceUpdater(wurflDevice, staticCaps, virtualCaps, true, mapper); diff --git a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java index 8d82ebb0209..f981f43ccaa 100644 --- a/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java +++ b/extra/modules/wurfl-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java @@ -8,6 +8,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.settings.model.Account; import org.prebid.server.json.JacksonMapper; @@ -20,6 +23,7 @@ import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.model.CaseInsensitiveMultiMap; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.Collections; import java.util.Map; @@ -87,6 +91,87 @@ public void callShouldReturnNoActionWhenDeviceIsNull() { assertThat(result.action()).isEqualTo(InvocationAction.no_action); } + @Test + public void callShouldReturnNoActionWhenDeviceHasWurflProperty() { + // given + final String ua = "Mozilla/5.0 (testPhone; CPU testPhone OS 1_0_2) Version/17.4.1 Mobile/12E GrandTest/604.1"; + final ExtDevicePrebid extDevicePrebid = ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)); + final ExtDevice extDevice = ExtDevice.of(0, extDevicePrebid); + final ObjectNode wurfl = mapper.mapper().createObjectNode(); + wurfl.put("wurfl_id", "test_phone_ver1"); + extDevice.addProperty("wurfl", wurfl); + final Device device = Device.builder() + .ua(ua) + .ext(extDevice) + .build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(payload.bidRequest()).willReturn(bidRequest); + given(configProperties.getAllowedPublisherIds()).willReturn(Collections.emptySet()); + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + public void callShouldReturnNoActionWhenDeviceHasDeviceTypeAndHwv() { + // given + final String ua = "Mozilla/5.0 (testPhone; CPU testPhone OS 1_0_2) Version/17.4.1 Mobile/12E GrandTest/604.1"; + final Device device = Device.builder() + .ua(ua) + .hwv("test_phone_ver1") + .devicetype(1) + .build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(payload.bidRequest()).willReturn(bidRequest); + given(configProperties.getAllowedPublisherIds()).willReturn(Collections.emptySet()); + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + given(wurflDevice.getId()).willReturn("test_phone_ver1"); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + public void callShouldReturnActionUpdateWhenDeviceHasJustDeviceType() { + // given + final String ua = "Mozilla/5.0 (testPhone; CPU testPhone OS 1_0_2) Version/17.4.1 Mobile/12E GrandTest/604.1"; + given(configProperties.getAllowedPublisherIds()).willReturn(Collections.emptySet()); + final WURFLService wurflService = new WURFLService(wurflEngine, configProperties); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflService, configProperties, mapper); + final Device device = Device.builder() + .ua(ua) + .devicetype(1) + .build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + given(payload.bidRequest()).willReturn(bidRequest); + final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder() + .add("User-Agent", ua) + .build(); + final AuctionRequestHeadersContext headersContext = AuctionRequestHeadersContext.from(headers); + + given(context.moduleContext()).willReturn(headersContext); + given(wurflEngine.getDeviceForRequest(any(Map.class))).willReturn(wurflDevice); + given(wurflDevice.getId()).willReturn("test_phone_ver1"); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + } + @Test public void callShouldUpdateDeviceWhenWurflDeviceIsDetected() throws Exception { // given diff --git a/sample/configs/prebid-config-with-wurfl.yaml b/sample/configs/prebid-config-with-wurfl.yaml index 5dff90201ee..0ce8abebe17 100644 --- a/sample/configs/prebid-config-with-wurfl.yaml +++ b/sample/configs/prebid-config-with-wurfl.yaml @@ -23,6 +23,7 @@ settings: filesystem: settings-filename: sample/configs/sample-app-settings.yaml stored-requests-dir: sample + profiles-dir: sample stored-imps-dir: sample stored-responses-dir: sample categories-dir: From 19e3285db0e3ccec315c1c6cee6ded9f23f8560f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriela=20Mi=C4=99dlar?= <155444733+gmiedlar-ox@users.noreply.github.com> Date: Fri, 26 Sep 2025 12:57:45 +0200 Subject: [PATCH 3/3] OpenX Adapter: Set buyer exts fields in `bid.ext.prebid.meta` (#4171) --- .../server/bidder/openx/OpenxBidder.java | 55 ++++++- .../bidder/openx/proto/OpenxBidExt.java | 15 ++ .../server/bidder/openx/OpenxBidderTest.java | 150 ++++++++++++++++++ 3 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/prebid/server/bidder/openx/proto/OpenxBidExt.java diff --git a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java index 09412abe65f..00a8c8dc3fb 100644 --- a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java +++ b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java @@ -17,6 +17,7 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; import org.prebid.server.bidder.openx.model.OpenxImpType; +import org.prebid.server.bidder.openx.proto.OpenxBidExt; import org.prebid.server.bidder.openx.proto.OpenxBidResponse; import org.prebid.server.bidder.openx.proto.OpenxBidResponseExt; import org.prebid.server.bidder.openx.proto.OpenxRequestExt; @@ -29,6 +30,8 @@ import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.openx.ExtImpOpenx; import org.prebid.server.proto.openrtb.ext.response.BidType; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid; +import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta; import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidVideo; import org.prebid.server.proto.openrtb.ext.response.ExtIgi; import org.prebid.server.proto.openrtb.ext.response.ExtIgiIgs; @@ -270,13 +273,13 @@ private ObjectNode makeImpExt(ObjectNode impExt, boolean addCustomParams) { return openxImpExt; } - private static List extractBids(BidRequest bidRequest, OpenxBidResponse bidResponse) { + private List extractBids(BidRequest bidRequest, OpenxBidResponse bidResponse) { return bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid()) ? Collections.emptyList() : bidsFromResponse(bidRequest, bidResponse); } - private static List bidsFromResponse(BidRequest bidRequest, OpenxBidResponse bidResponse) { + private List bidsFromResponse(BidRequest bidRequest, OpenxBidResponse bidResponse) { final Map impIdToBidType = impIdToBidType(bidRequest); final String bidCurrency = StringUtils.isNotBlank(bidResponse.getCur()) @@ -292,11 +295,11 @@ private static List bidsFromResponse(BidRequest bidRequest, OpenxBidR .toList(); } - private static BidderBid toBidderBid(Bid bid, Map impIdToBidType, String bidCurrency) { + private BidderBid toBidderBid(Bid bid, Map impIdToBidType, String bidCurrency) { final BidType bidType = getBidType(bid, impIdToBidType); final ExtBidPrebidVideo videoInfo = bidType == BidType.video ? getVideoInfo(bid) : null; return BidderBid.builder() - .bid(bid) + .bid(bid.toBuilder().ext(getBidExt(bid)).build()) .type(bidType) .bidCurrency(bidCurrency) .videoInfo(videoInfo) @@ -334,4 +337,48 @@ private static List extractIgi(OpenxBidResponse bidResponse) { return igs.isEmpty() ? null : Collections.singletonList(ExtIgi.builder().igs(igs).build()); } + + private ObjectNode getBidExt(Bid bid) { + final ObjectNode ext = bid.getExt(); + if (ext == null) { + return null; + } + + final OpenxBidExt openxBidExt = parseOpenxBidExt(ext); + final Integer buyerId = parseStringToInt(openxBidExt.getBuyerId()); + final Integer dspId = parseStringToInt(openxBidExt.getDspId()); + final Integer brandId = parseStringToInt(openxBidExt.getBrandId()); + + if (buyerId == null && dspId == null && brandId == null) { + return ext; + } + + final ExtBidPrebidMeta meta = ExtBidPrebidMeta.builder() + .networkId(dspId) + .advertiserId(buyerId) + .brandId(brandId) + .build(); + + final ExtBidPrebid extBidPrebid = ExtBidPrebid.builder().meta(meta).build(); + + ext.set(PREBID_EXT, mapper.mapper().valueToTree(extBidPrebid)); + + return ext; + } + + private OpenxBidExt parseOpenxBidExt(ObjectNode ext) { + try { + return mapper.mapper().convertValue(ext, OpenxBidExt.class); + } catch (IllegalArgumentException e) { + return OpenxBidExt.builder().build(); + } + } + + private static Integer parseStringToInt(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return null; + } + } } diff --git a/src/main/java/org/prebid/server/bidder/openx/proto/OpenxBidExt.java b/src/main/java/org/prebid/server/bidder/openx/proto/OpenxBidExt.java new file mode 100644 index 00000000000..00305cfb64b --- /dev/null +++ b/src/main/java/org/prebid/server/bidder/openx/proto/OpenxBidExt.java @@ -0,0 +1,15 @@ +package org.prebid.server.bidder.openx.proto; + +import lombok.Builder; +import lombok.Value; + +@Builder +@Value +public class OpenxBidExt { + + String dspId; + + String buyerId; + + String brandId; +} diff --git a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java index 461f300c051..fab9bedceb7 100644 --- a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java @@ -23,6 +23,7 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; +import org.prebid.server.bidder.openx.proto.OpenxBidExt; import org.prebid.server.bidder.openx.proto.OpenxBidResponse; import org.prebid.server.bidder.openx.proto.OpenxBidResponseExt; import org.prebid.server.bidder.openx.proto.OpenxRequestExt; @@ -983,6 +984,155 @@ public void makeBidsShouldReturnResultContainingEmptyValueAndErrorsWhenSeatBidEm .containsOnly(Collections.emptyList(), Collections.emptyList()); } + @Test + public void makeBidShouldReturnBidWithExtPrebidMetaContainingAllFieldsFromBidExt() throws JsonProcessingException { + // given + final ObjectNode bidExt = mapper.valueToTree(OpenxBidExt.builder() + .dspId("1") + .buyerId("2") + .brandId("3") + .build()); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString( + BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(List.of( + Bid.builder() + .w(200) + .h(150) + .price(BigDecimal.ONE) + .impid("impId1") + .dealid("dealid") + .adm("
This is an Ad
") + .ext(bidExt) + .build())) + .build())) + .build())); + + final BidRequest bidRequest = BidRequest.builder() + .id("bidRequestId") + .imp(List.of( + Imp.builder() + .id("impId1") + .banner(Banner.builder().build()) + .build())) + .build(); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, bidRequest); + + // then + final ObjectNode expectedExtWithBidMeta = mapper.createObjectNode() + .put("dsp_id", "1") + .put("buyer_id", "2") + .put("brand_id", "3") + .set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode() + .put("advertiserId", 2) + .put("brandId", 3) + .put("networkId", 1))); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getExt) + .containsExactly(expectedExtWithBidMeta); + } + + @Test + public void makeBidShouldReturnBidWithExtPrebidMetaContainingBrandIdFieldOnly() throws JsonProcessingException { + // given + final ObjectNode bidExt = mapper.valueToTree(OpenxBidExt.builder() + .brandId("4") + .build()); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString( + BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(List.of( + Bid.builder() + .w(200) + .h(150) + .price(BigDecimal.ONE) + .impid("impId1") + .dealid("dealid") + .adm("
This is an Ad
") + .ext(bidExt) + .build())) + .build())) + .build())); + + final BidRequest bidRequest = BidRequest.builder() + .id("bidRequestId") + .imp(List.of( + Imp.builder() + .id("impId1") + .banner(Banner.builder().build()) + .build())) + .build(); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, bidRequest); + + // then + final ObjectNode expectedExtWithBidMeta = mapper.createObjectNode() + .put("brand_id", "4") + .set("prebid", mapper.createObjectNode() + .set("meta", mapper.createObjectNode() + .put("brandId", 4))); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getExt) + .containsExactly(expectedExtWithBidMeta); + } + + @Test + public void makeBidShouldReturnBidWithExtPrebidMetaNotContainingFieldsWithInvalidValues() + throws JsonProcessingException { + // given + final ObjectNode bidExt = mapper.valueToTree(OpenxBidExt.builder() + .dspId("abc") + .buyerId("xyz") + .brandId("cba") + .build()); + final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString( + BidResponse.builder() + .seatbid(singletonList(SeatBid.builder() + .bid(List.of( + Bid.builder() + .w(200) + .h(150) + .price(BigDecimal.ONE) + .impid("impId1") + .dealid("dealid") + .adm("
This is an Ad
") + .ext(bidExt) + .build())) + .build())) + .build())); + + final BidRequest bidRequest = BidRequest.builder() + .id("bidRequestId") + .imp(List.of( + Imp.builder() + .id("impId1") + .banner(Banner.builder().build()) + .build())) + .build(); + + // when + final CompositeBidderResponse result = target.makeBidderResponse(httpCall, bidRequest); + + // then + final ObjectNode expectedExtWithBidMeta = mapper.createObjectNode() + .put("dsp_id", "abc") + .put("buyer_id", "xyz") + .put("brand_id", "cba"); + assertThat(result.getErrors()).isEmpty(); + assertThat(result.getBids()).hasSize(1) + .extracting(BidderBid::getBid) + .extracting(Bid::getExt) + .containsExactly(expectedExtWithBidMeta); + } + private static Map givenCustomParams(String key, Object values) { return singletonMap(key, mapper.valueToTree(values)); }