From f40ec3c8a7a96457b401f795b1417394ac943e8a Mon Sep 17 00:00:00 2001 From: andrea Date: Fri, 17 Jan 2025 10:38:16 +0100 Subject: [PATCH 1/8] WURFL-devicedetection module implementation (compatible with PBS Java 3.18+) --- extra/bundle/pom.xml | 7 + extra/modules/WURFL-devicedetection/README.md | 247 +++++++++++++++ extra/modules/WURFL-devicedetection/pom.xml | 29 ++ .../sample/request_data.json | 119 ++++++++ .../WURFLDeviceDetectionConfigProperties.java | 51 ++++ .../WURFLDeviceDetectionConfiguration.java | 37 +++ .../WURFLModuleConfigurationException.java | 8 + .../model/AuctionRequestHeadersContext.java | 31 ++ .../model/WURFLEngineInitializer.java | 112 +++++++ .../resolver/HeadersResolver.java | 132 ++++++++ .../resolver/PlatformNameVersion.java | 33 ++ .../devicedetection/v1/AccountValidator.java | 31 ++ .../devicedetection/v1/ExtWURFLMapper.java | 65 ++++ .../devicedetection/v1/OrtbDeviceUpdater.java | 259 ++++++++++++++++ .../WURFLDeviceDetectionEntrypointHook.java | 37 +++ .../v1/WURFLDeviceDetectionModule.java | 29 ++ ...LDeviceDetectionRawAuctionRequestHook.java | 143 +++++++++ .../module-config/WURFL-devicedetection.yaml | 46 +++ ...FLDeviceDetectionConfigPropertiesTest.java | 46 +++ .../devicedetection/mock/WURFLDeviceMock.java | 282 ++++++++++++++++++ .../AuctionRequestHeadersContextTest.java | 65 ++++ .../model/WURFLEngineInitializerTest.java | 125 ++++++++ .../resolver/HeadersResolverTest.java | 199 ++++++++++++ .../resolver/PlatformNameVersionTest.java | 69 +++++ .../v1/AccountValidatorTest.java | 110 +++++++ .../v1/ExtWURFLMapperTest.java | 131 ++++++++ .../v1/OrtbDeviceUpdaterTest.java | 243 +++++++++++++++ ...URFLDeviceDetectionEntrypointHookTest.java | 81 +++++ .../v1/WURFLDeviceDetectionModuleTest.java | 40 +++ ...iceDetectionRawAuctionRequestHookTest.java | 124 ++++++++ extra/modules/pom.xml | 1 + sample/configs/prebid-config-with-wurfl.yaml | 80 +++++ 32 files changed, 3012 insertions(+) create mode 100644 extra/modules/WURFL-devicedetection/README.md create mode 100644 extra/modules/WURFL-devicedetection/pom.xml create mode 100644 extra/modules/WURFL-devicedetection/sample/request_data.json create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigProperties.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfiguration.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLModuleConfigurationException.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContext.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializer.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolver.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersion.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidator.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapper.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHook.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModule.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java create mode 100644 extra/modules/WURFL-devicedetection/src/main/resources/module-config/WURFL-devicedetection.yaml create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigPropertiesTest.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/mock/WURFLDeviceMock.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContextTest.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializerTest.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolverTest.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersionTest.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidatorTest.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapperTest.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHookTest.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModuleTest.java create mode 100644 extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java create mode 100644 sample/configs/prebid-config-with-wurfl.yaml diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index 9eb4d2aaf33..4a5ca3144ed 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -55,6 +55,13 @@ pb-request-correction ${project.version} + diff --git a/extra/modules/WURFL-devicedetection/README.md b/extra/modules/WURFL-devicedetection/README.md new file mode 100644 index 00000000000..86eb9cd8f6b --- /dev/null +++ b/extra/modules/WURFL-devicedetection/README.md @@ -0,0 +1,247 @@ +## WURFL-devicedetection module + +The **WURFL Device Enrichment Module** for Prebid Server enhances the OpenRTB 2.x payload +with comprehensive device detection data powered by **ScientiaMobile**’s WURFL device detection framework. +Thanks to WURFL's device database, the module provides accurate and comprehensive device-related information, +enabling bidders to make better-informed targeting and optimization decisions. + +### Key features + +#### Device Field Enrichment: + +The module populates missing or empty fields in ortb2.device with the following data: + - **make**: Manufacturer of the device (e.g., "Apple", "Samsung"). + - **model**: Device model (e.g., "iPhone 14", "Galaxy S22"). + - **os**: Operating system (e.g., "iOS", "Android"). + - **osv**: Operating system version (e.g., "16.0", "12.0"). + - **h**: Screen height in pixels. + - **w**: Screen width in pixels. + - **ppi**: Screen pixels per inch (PPI). + - **pixelratio**: Screen pixel density ratio. + - **devicetype**: Device type (e.g., mobile, tablet, desktop). + - **Note**: If these fields are already populated in the bid request, the module will not overwrite them. +#### Publisher-Specific Enrichment: + +Device enrichment is selectively enabled for publishers based on their account ID. +The module identifies publishers through the `getAccount()` method in the `AuctionContext` class. + + +### Build prerequisites + +To build the WURFL device detection module, you need to download the WURFL Onsite Java API (both JAR and POM files) +from the Scientiamobile private repository and install it in your local Maven repository. +Access to the WURFL Onsite Java API repository requires a valid Scientiamobile WURFL license. +For more details, visit: [Scientiamobile WURFL Onsite API for Java](https://www.scientiamobile.com/secondary-products/wurfl-onsite-api-for-java/). + +Run the following command to install the WURFL API: + +```bash +mvn install:install-file \ + -Dfile= \ + -DgroupId=com.scientiamobile.wurfl \ + -DartifactId=wurfl \ + -Dversion= \ + -Dpackaging=jar \ + -DpomFile= +``` + +### Activating the WURFL Device Detection Module + +The WURFL device detection module is disabled by default. Building the Prebid Server Java with the default bundle option +does not include the WURFL module in the server's JAR file. + +To include the WURFL device detection module in the Prebid Server Java bundle, follow these steps: + +1. Uncomment the WURFL Java API dependency in `extra/modules/WURFL-devicedetection/pom.xml`. +2. Uncomment the WURFL module dependency in `extra/bundle/pom.xml`. +3. Uncomment the WURFL module name in the module list in `extra/modules/pom.xml`. + +After making these changes, you can build the Prebid Server Java bundle with the WURFL module using the following command: + +```bash +mvn clean package --file extra/pom.xml +``` + +### Configuring the WURFL Device Detection Module + +Below is a sample configuration for the WURFL device detection module: + +```yaml +hooks: + wurfl-devicedetection: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "entrypoint": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "wurfl-devicedetection", + "hook_impl_code": "wurfl-devicedetection-entrypoint-hook" + } + ] + } + ] + }, + "raw_auction_request": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "wurfl-devicedetection", + "hook_impl_code": "wurfl-devicedetection-raw-auction-request" + } + ] + } + ] + } + } + } + } + } + modules: + wurfl-devicedetection: + wurfl-file-dir-path: + wurfl-snapshot-url: https://data.scientiamobile.com//wurfl.zip + cache-size: 200000 + wurfl-run-updater: true + allowed-publisher-ids: 1 + ext-caps: false +``` + +### Configuration Options + +- **`wurfl-file-dir-path`** (Mandatory): Path to the directory where the WURFL file is downloaded. Directory must exist and be writable. +- **`wurfl-file-name`** (Mandatory): Name of the WURFL file, typically `wurfl.zip`. +- **`wurfl-file-url`** (Mandatory): URL to the licensed WURFL file to be downloaded when Prebid Server Java starts. +- **`cache-size`** (Optional): Maximum number of devices stored in the WURFL cache. Defaults to the WURFL cache's standard size. +- **`ext_caps`** (Optional): If `true`, the module adds all licensed capabilities to the `device.ext` object. +- **`wurfl-updater-frequency`** (Optional): Frequency for updating the WURFL file. Defaults to no updates. +- **`allowed_publisher_ids`** (Optional): List of publisher IDs permitted to use the module. Defaults to all publishers. + +A valid WURFL license must include all the required capabilities for device enrichment. + +### Launching Prebid Server Java with the WURFL Module + +After configuring the module and successfully building the Prebid Server bundle, start the server with the following command: + +```bash +java -jar target/prebid-server-bundle.jar --spring.config.additional-location=sample/configs/prebid-config-with-wurfl.yaml +``` + +This sample configuration contains the module hook basic configuration. All the other module configuration options +are located in the `WURFL-devicedetection.yaml` inside the module. + +When the server starts, it downloads the WURFL file from the `wurfl-file-url` and loads it into the module. + +Sample request data for testing is available in the module's `sample` directory. Using the `auction` endpoint, +you can observe WURFL-enriched device data in the response. + +### Sample Response + +Using the sample request data via `curl` when the module is configured with `ext_caps` set to `false` (or no value) + +```bash +curl http://localhost:8080/openrtb2/auction --data @extra/modules/WURFL-devicedetection/sample/request_data.json +``` + +the device object in the response will include WURFL device detection data: + +```json +"device": { + "ua": "Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro XL Build/AP3A.241005.015;) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 EdgA/124.0.2478.64", + "devicetype": 1, + "make": "Google", + "model": "Pixel 9 Pro XL", + "os": "Android", + "osv": "15", + "h": 2992, + "w": 1344, + "ppi": 481, + "pxratio": 2.55, + "js": 1, + "ext": { + "wurfl": { + "wurfl_id": "google_pixel_9_pro_xl_ver1_suban150" + } + } +} +``` + +When `ext_caps` is set to `true`, the response will include all licensed capabilities: + +```json +"device":{ + "ua":"Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro XL Build/AP3A.241005.015; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 EdgA/124.0.2478.64", + "devicetype":1, + "make":"Google", + "model":"Pixel 9 Pro XL", + "os":"Android", + "osv":"15", + "h":2992, + "w":1344, + "ppi":481, + "pxratio":2.55, + "js":1, + "ext":{ + "wurfl":{ + "wurfl_id":"google_pixel_9_pro_xl_ver1_suban150", + "mobile_browser_version":"", + "resolution_height":"2992", + "resolution_width":"1344", + "is_wireless_device":"true", + "is_tablet":"false", + "physical_form_factor":"phone_phablet", + "ajax_support_javascript":"true", + "preferred_markup":"html_web_4_0", + "brand_name":"Google", + "can_assign_phone_number":"true", + "xhtml_support_level":"4", + "ux_full_desktop":"false", + "device_os":"Android", + "physical_screen_width":"71", + "is_connected_tv":"false", + "is_smarttv":"false", + "physical_screen_height":"158", + "model_name":"Pixel 9 Pro XL", + "is_ott":"false", + "density_class":"2.55", + "marketing_name":"", + "device_os_version":"15.0", + "mobile_browser":"Chrome Mobile", + "pointing_method":"touchscreen", + "is_app_webview":"false", + "advertised_app_name":"Edge Browser", + "is_smartphone":"true", + "is_robot":"false", + "advertised_device_os":"Android", + "is_largescreen":"true", + "is_android":"true", + "is_xhtmlmp_preferred":"false", + "device_name":"Google Pixel 9 Pro XL", + "is_ios":"false", + "is_touchscreen":"true", + "is_wml_preferred":"false", + "is_app":"false", + "is_mobile":"true", + "is_phone":"true", + "is_full_desktop":"false", + "is_generic":"false", + "advertised_browser":"Edge", + "complete_device_name":"Google Pixel 9 Pro XL", + "advertised_browser_version":"124.0.2478.64", + "is_html_preferred":"true", + "is_windows_phone":"false", + "pixel_density":"481", + "form_factor":"Smartphone", + "advertised_device_os_version":"15" + } + } +} +``` diff --git a/extra/modules/WURFL-devicedetection/pom.xml b/extra/modules/WURFL-devicedetection/pom.xml new file mode 100644 index 00000000000..da087b44cca --- /dev/null +++ b/extra/modules/WURFL-devicedetection/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.19.0-SNAPSHOT + + + wurfl-devicedetection + + wurfl-devicedetection + WURFL device detection and data enrichment module + + + 1.13.2.1 + + + + + + diff --git a/extra/modules/WURFL-devicedetection/sample/request_data.json b/extra/modules/WURFL-devicedetection/sample/request_data.json new file mode 100644 index 00000000000..5184f4ca6f3 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/sample/request_data.json @@ -0,0 +1,119 @@ +{ + "imp": [ + { + "ext": { + "data": { + "adserver": { + "name": "gam", + "adslot": "test" + }, + "pbadslot": "test", + "gpid": "test" + }, + "gpid": "test", + "prebid": { + "bidder": { + "appnexus": { + "placement_id": 1, + "use_pmt_rule": false + }, + "0test": { + "placement_id": 1 + } + }, + "adunitcode": "25e8ad9f-13a4-4404-ba74-f9eebff0e86c", + "floors": { + "floorMin": 0.01 + } + } + }, + "id": "2529eeea-813e-4da6-838f-f91c28d64867", + "banner": { + "topframe": 1, + "format": [ + { + "w": 728, + "h": 90 + } + ], + "pos": 1 + }, + "bidfloor": 0.01, + "bidfloorcur": "USD" + } + ], + "site": { + "domain": "test.com", + "publisher": { + "domain": "test.com", + "id": "3" + }, + "page": "https://www.test.com/" + }, + "device": { + "ua": "Mozilla/5.0 (Linux; Android 15; Pixel 9 Pro XL Build/AP3A.241005.015; ) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36 EdgA/124.0.2478.64" + }, + "id": "fc4670ce-4985-4316-a245-b43c885dc37a", + "test": 1, + "cur": [ + "USD" + ], + "source": { + "ext": { + "schain": { + "ver": "1.0", + "complete": 1, + "nodes": [ + { + "asi": "example.com", + "sid": "1234", + "hp": 1 + } + ] + } + } + }, + "ext": { + "prebid": { + "cache": { + "bids": { + "returnCreative": true + }, + "vastxml": { + "returnCreative": true + } + }, + "auctiontimestamp": 1799310801804, + "targeting": { + "includewinners": true, + "includebidderkeys": false + }, + "schains": [ + { + "bidders": [ + "appnexus" + ], + "schain": { + "ver": "1.0", + "complete": 1, + "nodes": [ + { + "asi": "example.com", + "sid": "1234", + "hp": 1 + } + ] + } + } + ], + "floors": { + "enabled": false, + "floorMin": 0.01, + "floorMinCur": "USD" + }, + "createtids": false + } + }, + "user": {}, + "tmax": 2000 +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigProperties.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigProperties.java new file mode 100644 index 00000000000..e9d60e6880f --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigProperties.java @@ -0,0 +1,51 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config; + +import lombok.Data; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionModule; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; +import java.util.Set; + +@ConfigurationProperties(prefix = "hooks.modules." + WURFLDeviceDetectionModule.CODE) +@Data + +public class WURFLDeviceDetectionConfigProperties { + + public static final Set REQUIRED_STATIC_CAPS = Set.of( + "ajax_support_javascript", + "brand_name", + "density_class", + "is_connected_tv", + "is_ott", + "is_tablet", + "model_name", + "resolution_height", + "resolution_width", + "physical_form_factor" + ); + + public static final Set REQUIRED_VIRTUAL_CAPS = Set.of( + + "advertised_device_os", + "advertised_device_os_version", + "complete_device_name", + "is_full_desktop", + "is_mobile", + "is_phone", + "form_factor", + "pixel_density" + ); + + int cacheSize; + + String wurflFileDirPath; + + String wurflSnapshotUrl; + + boolean extCaps; + + boolean wurflRunUpdater = true; + + List allowedPublisherIds = List.of(); +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfiguration.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfiguration.java new file mode 100644 index 00000000000..770ae1cd88f --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfiguration.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config; + +import com.scientiamobile.wurfl.core.WURFLEngine; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.WURFLEngineInitializer; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionEntrypointHook; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionModule; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1.WURFLDeviceDetectionRawAuctionRequestHook; +import org.prebid.server.spring.env.YamlPropertySourceFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +import java.util.List; + +@ConditionalOnProperty(prefix = "hooks." + WURFLDeviceDetectionModule.CODE, name = "enabled", havingValue = "true") +@Configuration +@PropertySource( + value = "classpath:/module-config/WURFL-devicedetection.yaml", + factory = YamlPropertySourceFactory.class) +@EnableConfigurationProperties(WURFLDeviceDetectionConfigProperties.class) +public class WURFLDeviceDetectionConfiguration { + + @Bean + public WURFLDeviceDetectionModule wurflDeviceDetectionModule(WURFLDeviceDetectionConfigProperties + configProperties) { + + final WURFLEngine wurflEngine = WURFLEngineInitializer.builder() + .configProperties(configProperties) + .build().initWURFLEngine(); + wurflEngine.load(); + + return new WURFLDeviceDetectionModule(List.of(new WURFLDeviceDetectionEntrypointHook(), + new WURFLDeviceDetectionRawAuctionRequestHook(wurflEngine, configProperties))); + } +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLModuleConfigurationException.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLModuleConfigurationException.java new file mode 100644 index 00000000000..d2c767c45c6 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/exc/WURFLModuleConfigurationException.java @@ -0,0 +1,8 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.exc; + +public class WURFLModuleConfigurationException extends RuntimeException { + + public WURFLModuleConfigurationException(String message) { + super(message); + } +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContext.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContext.java new file mode 100644 index 00000000000..95a32fbfabe --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContext.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model; + +import lombok.Getter; +import org.prebid.server.model.CaseInsensitiveMultiMap; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +@Getter +public class AuctionRequestHeadersContext { + + Map headers; + + private AuctionRequestHeadersContext(Map headers) { + this.headers = headers; + } + + public static AuctionRequestHeadersContext from(final CaseInsensitiveMultiMap headers) { + final Map headersMap = new HashMap<>(); + if (Objects.isNull(headers)) { + return new AuctionRequestHeadersContext(headersMap); + } + + for (String headerName : headers.names()) { + headersMap.put(headerName, headers.getAll(headerName).getFirst()); + } + return new AuctionRequestHeadersContext(headersMap); + } + +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializer.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializer.java new file mode 100644 index 00000000000..45e288dad33 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializer.java @@ -0,0 +1,112 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model; + +import com.scientiamobile.wurfl.core.GeneralWURFLEngine; +import com.scientiamobile.wurfl.core.WURFLEngine; +import com.scientiamobile.wurfl.core.cache.LRUMapCacheProvider; +import com.scientiamobile.wurfl.core.cache.NullCacheProvider; +import com.scientiamobile.wurfl.core.updater.Frequency; +import com.scientiamobile.wurfl.core.updater.WURFLUpdater; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.exc.WURFLModuleConfigurationException; + +import java.net.URI; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Builder +public class WURFLEngineInitializer { + + private WURFLDeviceDetectionConfigProperties configProperties; + + public WURFLEngine initWURFLEngine() { + downloadWurflFile(configProperties); + final WURFLEngine engine = initializeEngine(configProperties); + setupUpdater(configProperties, engine); + return engine; + } + + static void downloadWurflFile(WURFLDeviceDetectionConfigProperties configProperties) { + if (StringUtils.isNotBlank(configProperties.getWurflSnapshotUrl()) + && StringUtils.isNotBlank(configProperties.getWurflFileDirPath())) { + GeneralWURFLEngine.wurflDownload( + configProperties.getWurflSnapshotUrl(), + configProperties.getWurflFileDirPath()); + } + } + + static WURFLEngine initializeEngine(WURFLDeviceDetectionConfigProperties configProperties) { + + final String wurflFileName = extractWURFLFileName(configProperties.getWurflSnapshotUrl()); + + final Path wurflPath = Paths.get( + configProperties.getWurflFileDirPath(), + wurflFileName + ); + final WURFLEngine engine = new GeneralWURFLEngine(wurflPath.toString()); + verifyStaticCapabilitiesDefinition(engine); + + if (configProperties.getCacheSize() > 0) { + engine.setCacheProvider(new LRUMapCacheProvider(configProperties.getCacheSize())); + } else { + engine.setCacheProvider(new NullCacheProvider()); + } + return engine; + } + + private static String extractWURFLFileName(String wurflSnapshotUrl) { + + try { + final URI uri = new URI(wurflSnapshotUrl); + final String path = uri.getPath(); + return path.substring(path.lastIndexOf('/') + 1); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid WURFL snapshot URL: " + wurflSnapshotUrl, e); + } + } + + static void verifyStaticCapabilitiesDefinition(WURFLEngine engine) { + + final List unsupportedStaticCaps = new ArrayList<>(); + final Map allCaps = engine.getAllCapabilities().stream() + .collect(Collectors.toMap( + key -> key, + value -> true + )); + + for (String requiredCapName : WURFLDeviceDetectionConfigProperties.REQUIRED_STATIC_CAPS) { + if (!allCaps.containsKey(requiredCapName)) { + unsupportedStaticCaps.add(requiredCapName); + } + } + + if (!unsupportedStaticCaps.isEmpty()) { + Collections.sort(unsupportedStaticCaps); + final String failedCheckMessage = """ + Static capabilities %s needed for device enrichment are not defined in WURFL. + Please make sure that your license has the needed capabilities or upgrade it. + """.formatted(String.join(",", unsupportedStaticCaps)); + + throw new WURFLModuleConfigurationException(failedCheckMessage); + } + + } + + static void setupUpdater(WURFLDeviceDetectionConfigProperties configProperties, WURFLEngine engine) { + final boolean runUpdater = configProperties.isWurflRunUpdater(); + + if (runUpdater) { + final WURFLUpdater updater = new WURFLUpdater(engine, configProperties.getWurflSnapshotUrl()); + updater.setFrequency(Frequency.DAILY); + updater.performPeriodicUpdate(); + } + } +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolver.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolver.java new file mode 100644 index 00000000000..5051b47a9dd --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolver.java @@ -0,0 +1,132 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver; + +import com.iab.openrtb.request.BrandVersion; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.UserAgent; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +public class HeadersResolver { + + static final String SEC_CH_UA = "Sec-CH-UA"; + static final String SEC_CH_UA_PLATFORM = "Sec-CH-UA-Platform"; + static final String SEC_CH_UA_PLATFORM_VERSION = "Sec-CH-UA-Platform-Version"; + static final String SEC_CH_UA_MOBILE = "Sec-CH-UA-Mobile"; + static final String SEC_CH_UA_ARCH = "Sec-CH-UA-Arch"; + static final String SEC_CH_UA_MODEL = "Sec-CH-UA-Model"; + static final String SEC_CH_UA_FULL_VERSION_LIST = "Sec-CH-UA-Full-Version-List"; + static final String USER_AGENT = "User-Agent"; + + public Map resolve(final Device device, final Map headers) { + + if (Objects.isNull(device) && Objects.isNull(headers)) { + return new HashMap<>(); + } + + final Map resolvedHeaders = resolveFromOrtbDevice(device); + if (MapUtils.isEmpty(resolvedHeaders)) { + return headers; + } + + return resolvedHeaders; + } + + private Map resolveFromOrtbDevice(Device device) { + + final Map resolvedHeaders = new HashMap<>(); + + if (Objects.isNull(device)) { + log.warn("ORBT Device is null"); + return resolvedHeaders; + } + + if (Objects.nonNull(device.getUa())) { + resolvedHeaders.put(USER_AGENT, device.getUa()); + } + + resolvedHeaders.putAll(resolveFromSua(device.getSua())); + return resolvedHeaders; + } + + private Map resolveFromSua(UserAgent sua) { + + final Map headers = new HashMap<>(); + + if (Objects.isNull(sua)) { + log.warn("Sua is null, returning empty headers"); + return new HashMap<>(); + } + + // Browser brands and versions + final List brands = sua.getBrowsers(); + if (CollectionUtils.isEmpty(brands)) { + log.warn("No browser brands and versions found"); + return headers; + } + + final String brandList = brandListAsString(brands); + headers.put(SEC_CH_UA, brandList); + headers.put(SEC_CH_UA_FULL_VERSION_LIST, brandList); + + // Platform + final PlatformNameVersion platformNameVersion = PlatformNameVersion.from(sua.getPlatform()); + if (Objects.nonNull(platformNameVersion)) { + headers.put(SEC_CH_UA_PLATFORM, escape(platformNameVersion.getPlatformName())); + headers.put(SEC_CH_UA_PLATFORM_VERSION, escape(platformNameVersion.getPlatformVersion())); + } + + // Model + final String model = sua.getModel(); + if (Objects.nonNull(model) && !model.isEmpty()) { + headers.put(SEC_CH_UA_MODEL, escape(model)); + } + + // Architecture + final String arch = sua.getArchitecture(); + if (Objects.nonNull(arch) && !arch.isEmpty()) { + headers.put(SEC_CH_UA_ARCH, escape(arch)); + } + + // Mobile + final Integer mobile = sua.getMobile(); + if (Objects.nonNull(mobile)) { + headers.put(SEC_CH_UA_MOBILE, "?" + mobile); + } + return headers; + } + + private String brandListAsString(List versions) { + + final String brandNameString = versions.stream() + .filter(brandVersion -> brandVersion.getBrand() != null) + .map(brandVersion -> { + final String brandName = escape(brandVersion.getBrand()); + final String versionString = versionFromTokens(brandVersion.getVersion()); + return brandName + ";v=\"" + versionString + "\""; + }) + .collect(Collectors.joining(", ")); + return brandNameString; + } + + private static String escape(String value) { + return '"' + value.replace("\"", "\\\"") + '"'; + } + + public static String versionFromTokens(List tokens) { + if (tokens == null || tokens.isEmpty()) { + return ""; + } + + return tokens.stream() + .filter(token -> token != null && !token.isEmpty()) + .collect(Collectors.joining(".")); + } +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersion.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersion.java new file mode 100644 index 00000000000..dc01aa127b2 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersion.java @@ -0,0 +1,33 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver; + +import com.iab.openrtb.request.BrandVersion; +import lombok.Getter; + +import java.util.Objects; + +public class PlatformNameVersion { + + @Getter + private String platformName; + + private String platformVersion; + + public static PlatformNameVersion from(BrandVersion platform) { + if (Objects.isNull(platform)) { + return null; + } + final PlatformNameVersion platformNameVersion = new PlatformNameVersion(); + platformNameVersion.platformName = platform.getBrand(); + platformNameVersion.platformVersion = HeadersResolver.versionFromTokens(platform.getVersion()); + return platformNameVersion; + } + + public String getPlatformVersion() { + return platformVersion; + } + + public String toString() { + return platformName + " " + platformVersion; + } + +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidator.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidator.java new file mode 100644 index 00000000000..0d930479fab --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidator.java @@ -0,0 +1,31 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.settings.model.Account; +import lombok.Builder; + +import java.util.Objects; +import java.util.Optional; +import java.util.Map; + +@Slf4j +@Builder +public class AccountValidator { + + Map allowedPublisherIds; + AuctionContext auctionContext; + + public boolean isAccountValid() { + + return Optional.ofNullable(auctionContext) + .map(AuctionContext::getAccount) + .map(Account::getId) + .filter(StringUtils::isNotBlank) + .map(allowedPublisherIds::get) + .filter(Objects::nonNull) + .isPresent(); + } + +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapper.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapper.java new file mode 100644 index 00000000000..33500ebfce6 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapper.java @@ -0,0 +1,65 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Builder; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Builder +@Slf4j +public class ExtWURFLMapper { + + private final List staticCaps; + private final List virtualCaps; + private boolean addExtCaps; + private final com.scientiamobile.wurfl.core.Device wurflDevice; + private static final String NULL_VALUE_TOKEN = "$null$"; + private static final String WURFL_ID_PROPERTY = "wurfl_id"; + + public JsonNode mapExtProperties() { + + final ObjectMapper objectMapper = new ObjectMapper(); + final ObjectNode wurflNode = objectMapper.createObjectNode(); + + try { + wurflNode.put(WURFL_ID_PROPERTY, wurflDevice.getId()); + + if (addExtCaps) { + + staticCaps.stream() + .map(sc -> { + try { + return Map.entry(sc, wurflDevice.getCapability(sc)); + } catch (Exception e) { + log.error("Error getting capability for {}: {}", sc, e.getMessage()); + return Map.entry(sc, NULL_VALUE_TOKEN); + } + }) + .filter(entry -> Objects.nonNull(entry.getValue()) + && !NULL_VALUE_TOKEN.equals(entry.getValue())) + .forEach(entry -> wurflNode.put(entry.getKey(), entry.getValue())); + + virtualCaps.stream() + .map(vc -> Map.entry(vc, wurflDevice.getVirtualCapability(vc))) + .filter(entry -> Objects.nonNull(entry.getValue())) + .forEach(entry -> wurflNode.put(entry.getKey(), entry.getValue())); + } + } catch (Exception e) { + log.error("Exception while updating EXT"); + } + + JsonNode node = null; + try { + node = objectMapper.readTree(wurflNode.toString()); + } catch (JsonProcessingException e) { + log.error("Error creating WURFL ext device JSON: {}", e.getMessage()); + } + return node; + } +} 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 new file mode 100644 index 00000000000..f8e9a9259c5 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdater.java @@ -0,0 +1,259 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.iab.openrtb.request.Device; +import com.scientiamobile.wurfl.core.exc.CapabilityNotDefinedException; +import com.scientiamobile.wurfl.core.exc.VirtualCapabilityNotDefinedException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.model.UpdateResult; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@Slf4j +public class OrtbDeviceUpdater { + + private static final String WURFL_PROPERTY = "wurfl"; + private static final Map NAME_TO_IS_VIRTUAL_CAPABILITY = Map.of( + "brand_name", false, + "model_name", false, + "resolution_width", false, + "resolution_height", false, + "advertised_device_os", true, + "advertised_device_os_version", true, + "pixel_density", true, + "density_class", false, + "ajax_support_javascript", false + ); + + public Device update(Device ortbDevice, com.scientiamobile.wurfl.core.Device wurflDevice, + List staticCaps, List virtualCaps, boolean addExtCaps) { + + final Device.DeviceBuilder deviceBuilder = ortbDevice.toBuilder(); + + // make + final UpdateResult updatedMake = tryUpdateStringField(ortbDevice.getMake(), wurflDevice, + "brand_name"); + if (updatedMake.isUpdated()) { + deviceBuilder.make(updatedMake.getValue()); + } + + // model + final UpdateResult updatedModel = tryUpdateStringField(ortbDevice.getModel(), wurflDevice, + "model_name"); + if (updatedModel.isUpdated()) { + deviceBuilder.model(updatedModel.getValue()); + } + + // deviceType + final UpdateResult updatedDeviceType = tryUpdateDeviceTypeField(ortbDevice.getDevicetype(), + getOrtb2DeviceType(wurflDevice)); + if (updatedDeviceType.isUpdated()) { + deviceBuilder.devicetype(updatedDeviceType.getValue()); + } + + // os + final UpdateResult updatedOS = tryUpdateStringField(ortbDevice.getOs(), wurflDevice, + "advertised_device_os"); + if (updatedOS.isUpdated()) { + deviceBuilder.os(updatedOS.getValue()); + } + + // os version + final UpdateResult updatedOsv = tryUpdateStringField(ortbDevice.getOsv(), wurflDevice, + "advertised_device_os_version"); + if (updatedOS.isUpdated()) { + deviceBuilder.osv(updatedOsv.getValue()); + } + + // h (resolution height) + final UpdateResult updatedH = tryUpdateIntegerField(ortbDevice.getH(), wurflDevice, + "resolution_height", false); + if (updatedH.isUpdated()) { + deviceBuilder.h(updatedH.getValue()); + } + + // w (resolution height) + final UpdateResult updatedW = tryUpdateIntegerField(ortbDevice.getW(), wurflDevice, + "resolution_width", false); + if (updatedW.isUpdated()) { + deviceBuilder.w(updatedW.getValue()); + } + + // Pixels per inch + final UpdateResult updatedPpi = tryUpdateIntegerField(ortbDevice.getPpi(), wurflDevice, + "pixel_density", false); + if (updatedPpi.isUpdated()) { + deviceBuilder.ppi(updatedPpi.getValue()); + } + + // Pixel ratio + final UpdateResult updatedPxRatio = tryUpdateBigDecimalField(ortbDevice.getPxratio(), wurflDevice, + "density_class"); + if (updatedPxRatio.isUpdated()) { + deviceBuilder.pxratio(updatedPxRatio.getValue()); + } + + // Javascript support + final UpdateResult updatedJs = tryUpdateIntegerField(ortbDevice.getJs(), wurflDevice, + "ajax_support_javascript", true); + if (updatedJs.isUpdated()) { + deviceBuilder.js(updatedJs.getValue()); + } + + // Ext + final ExtWURFLMapper extMapper = ExtWURFLMapper.builder() + .wurflDevice(wurflDevice) + .staticCaps(staticCaps) + .virtualCaps(virtualCaps) + .addExtCaps(addExtCaps) + .build(); + final ExtDevice updatedExt = ExtDevice.empty(); + final ExtDevice ortbDeviceExt = ortbDevice.getExt(); + + if (Objects.nonNull(ortbDeviceExt)) { + updatedExt.addProperties(ortbDeviceExt.getProperties()); + if (!ortbDeviceExt.containsProperty(WURFL_PROPERTY)) { + updatedExt.addProperty("wurfl", extMapper.mapExtProperties()); + } + } else { + updatedExt.addProperty("wurfl", extMapper.mapExtProperties()); + } + deviceBuilder.ext(updatedExt); + return deviceBuilder.build(); + } + + private UpdateResult tryUpdateStringField(String fromOrtbDevice, + com.scientiamobile.wurfl.core.Device wurflDevice, + String capName) { + if (StringUtils.isNotBlank(fromOrtbDevice)) { + return UpdateResult.unaltered(fromOrtbDevice); + } + + final String fromWurfl = isVirtualCapability(capName) + ? wurflDevice.getVirtualCapability(capName) + : wurflDevice.getCapability(capName); + + if (Objects.nonNull(fromWurfl)) { + return UpdateResult.updated(fromWurfl); + } + + return UpdateResult.unaltered(fromOrtbDevice); + } + + private UpdateResult tryUpdateIntegerField(Integer fromOrtbDevice, + com.scientiamobile.wurfl.core.Device wurflDevice, + String capName, boolean convertFromBool) { + if (Objects.nonNull(fromOrtbDevice)) { + return UpdateResult.unaltered(fromOrtbDevice); + } + + final String fromWurfl = isVirtualCapability(capName) + ? wurflDevice.getVirtualCapability(capName) + : wurflDevice.getCapability(capName); + + if (StringUtils.isNotBlank(fromWurfl)) { + + if (convertFromBool) { + return fromWurfl.equalsIgnoreCase("true") + ? UpdateResult.updated(1) + : UpdateResult.updated(0); + } + + return UpdateResult.updated(Integer.parseInt(fromWurfl)); + } + return UpdateResult.unaltered(fromOrtbDevice); + } + + private UpdateResult tryUpdateBigDecimalField(BigDecimal fromOrtbDevice, + com.scientiamobile.wurfl.core.Device wurflDevice, + String capName) { + + if (Objects.nonNull(fromOrtbDevice)) { + return UpdateResult.unaltered(fromOrtbDevice); + } + + final String fromWurfl = isVirtualCapability(capName) + ? wurflDevice.getVirtualCapability(capName) + : wurflDevice.getCapability(capName); + + if (Objects.nonNull(fromWurfl)) { + + BigDecimal pxRatio = null; + try { + pxRatio = new BigDecimal(fromWurfl); + return UpdateResult.updated(pxRatio); + } catch (NullPointerException e) { + log.warn("Cannot convert WURFL device pixel density {} to ortb device pxratio", pxRatio); + } + } + + return UpdateResult.unaltered(fromOrtbDevice); + } + + private boolean isVirtualCapability(String capName) { + return NAME_TO_IS_VIRTUAL_CAPABILITY.get(capName); + } + + private UpdateResult tryUpdateDeviceTypeField(Integer fromOrtbDevice, Integer fromWurfl) { + final boolean isNotNullAndPositive = Objects.nonNull(fromOrtbDevice) && fromOrtbDevice > 0; + if (isNotNullAndPositive) { + return UpdateResult.unaltered(fromOrtbDevice); + } + + if (Objects.nonNull(fromWurfl)) { + return UpdateResult.updated(fromWurfl); + } + + return UpdateResult.unaltered(fromOrtbDevice); + } + + public static Integer getOrtb2DeviceType(final com.scientiamobile.wurfl.core.Device wurflDevice) { + + final boolean isPhone; + final boolean isTablet; + + if (wurflDevice.getVirtualCapabilityAsBool("is_mobile")) { + // if at least one if these capabilities is not defined the mobile device type is undefined + try { + isPhone = wurflDevice.getVirtualCapabilityAsBool("is_phone"); + isTablet = wurflDevice.getCapabilityAsBool("is_tablet"); + } catch (CapabilityNotDefinedException | VirtualCapabilityNotDefinedException e) { + return null; + } + + if (isPhone || isTablet) { + return 1; + } + return 6; + } + + // desktop device + if (wurflDevice.getVirtualCapabilityAsBool("is_full_desktop")) { + return 2; + } + + // connected tv + if (wurflDevice.getCapabilityAsBool("is_connected_tv")) { + return 3; + } + + if (wurflDevice.getCapabilityAsBool("is_phone")) { + return 4; + } + + if (wurflDevice.getCapabilityAsBool("is_tablet")) { + return 5; + } + + if (wurflDevice.getCapabilityAsBool("is_ott")) { + return 7; + } + + return null; // Return null for undefined device type + } + +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHook.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHook.java new file mode 100644 index 00000000000..ccca8c4038b --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHook.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import lombok.extern.slf4j.Slf4j; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; +import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import io.vertx.core.Future; + +@Slf4j +public class WURFLDeviceDetectionEntrypointHook implements EntrypointHook { + + private static final String CODE = "wurfl-devicedetection-entrypoint-hook"; + + @Override + public Future> call( + EntrypointPayload entrypointPayload, InvocationContext invocationContext) { + + final AuctionRequestHeadersContext bidRequestHeadersContext = AuctionRequestHeadersContext.from( + entrypointPayload.headers()); + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .moduleContext(bidRequestHeadersContext) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModule.java b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModule.java new file mode 100644 index 00000000000..3bdaceff99f --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModule.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import org.prebid.server.hooks.v1.Module; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.util.Collection; +import java.util.List; + +public class WURFLDeviceDetectionModule implements Module { + + public static final String CODE = "wurfl-devicedetection"; + private final List> hooks; + + public WURFLDeviceDetectionModule(List> hooks) { + this.hooks = hooks; + + } + + @Override + public String code() { + return CODE; + } + + @Override + public Collection> hooks() { + return this.hooks; + } +} 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 new file mode 100644 index 00000000000..1824b4e520c --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHook.java @@ -0,0 +1,143 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.scientiamobile.wurfl.core.WURFLEngine; +import com.scientiamobile.wurfl.core.exc.CapabilityNotDefinedException; +import com.scientiamobile.wurfl.core.exc.VirtualCapabilityNotDefinedException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver.HeadersResolver; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; +import org.prebid.server.auction.model.AuctionContext; +import io.vertx.core.Future; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +public class WURFLDeviceDetectionRawAuctionRequestHook implements RawAuctionRequestHook { + + public static final String CODE = "wurfl-devicedetection-raw-auction-request"; + + private final WURFLEngine wurflEngine; + private final List staticCaps; + private final List virtualCaps; + private final OrtbDeviceUpdater ortbDeviceUpdater; + private final Map allowedPublisherIDs; + private final boolean addExtCaps; + + public WURFLDeviceDetectionRawAuctionRequestHook(WURFLEngine wurflEngine, + WURFLDeviceDetectionConfigProperties configProperties) { + this.wurflEngine = wurflEngine; + this.staticCaps = wurflEngine.getAllCapabilities().stream().toList(); + this.virtualCaps = safeGetVirtualCaps(wurflEngine); + this.ortbDeviceUpdater = new OrtbDeviceUpdater(); + this.addExtCaps = configProperties.isExtCaps(); + this.allowedPublisherIDs = configProperties.getAllowedPublisherIds().stream() + .collect(Collectors.toMap(item -> item, item -> item)); + } + + private List safeGetVirtualCaps(WURFLEngine wurflEngine) { + final List allVcaps = wurflEngine.getAllVirtualCapabilities().stream().toList(); + final List safeVcaps = new ArrayList<>(); + final var device = wurflEngine.getDeviceById("generic"); + allVcaps.forEach(vc -> { + try { + device.getVirtualCapability(vc); + safeVcaps.add(vc); + } catch (VirtualCapabilityNotDefinedException | CapabilityNotDefinedException ignored) { } + }); + return safeVcaps; + } + + @Override + public Future> call(AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + if (!shouldEnrichDevice(invocationContext)) { + return noUpdateResultFuture(); + } + + final BidRequest bidRequest = auctionRequestPayload.bidRequest(); + Device ortbDevice = null; + if (bidRequest == null) { + log.warn("BidRequest is null"); + return noUpdateResultFuture(); + } else { + ortbDevice = bidRequest.getDevice(); + if (ortbDevice == null) { + log.warn("Device is null"); + return noUpdateResultFuture(); + } + } + + final AuctionRequestHeadersContext headersContext; + Map requestHeaders = null; + if (invocationContext.moduleContext() instanceof AuctionRequestHeadersContext) { + headersContext = (AuctionRequestHeadersContext) invocationContext.moduleContext(); + if (headersContext != null) { + requestHeaders = headersContext.getHeaders(); + } + + final Map headers = new HeadersResolver().resolve(ortbDevice, requestHeaders); + final com.scientiamobile.wurfl.core.Device wurflDevice = wurflEngine.getDeviceForRequest(headers); + + try { + final Device updatedDevice = ortbDeviceUpdater.update(ortbDevice, wurflDevice, staticCaps, + virtualCaps, addExtCaps); + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(payload -> + AuctionRequestPayloadImpl.of(bidRequest.toBuilder() + .device(updatedDevice) + .build())) + .build() + ); + } catch (Exception e) { + log.error("Exception " + e.getMessage()); + } + + } + + return noUpdateResultFuture(); + } + + private static Future> noUpdateResultFuture() { + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .build()); + } + + private boolean shouldEnrichDevice(AuctionInvocationContext invocationContext) { + if (MapUtils.isEmpty(allowedPublisherIDs)) { + return true; + } + + final AuctionContext auctionContext = invocationContext.auctionContext(); + return AccountValidator.builder().allowedPublisherIds(allowedPublisherIDs) + .auctionContext(auctionContext) + .build() + .isAccountValid(); + } + + @Override + public String code() { + return CODE; + } + +} diff --git a/extra/modules/WURFL-devicedetection/src/main/resources/module-config/WURFL-devicedetection.yaml b/extra/modules/WURFL-devicedetection/src/main/resources/module-config/WURFL-devicedetection.yaml new file mode 100644 index 00000000000..e8c4f2a5229 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/main/resources/module-config/WURFL-devicedetection.yaml @@ -0,0 +1,46 @@ +hooks: + wurfl-devicedetection: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "entrypoint": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "wurfl-devicedetection", + "hook_impl_code": "wurfl-devicedetection-entrypoint-hook" + } + ] + } + ] + }, + "raw_auction_request": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "wurfl-devicedetection", + "hook_impl_code": "wurfl-devicedetection-raw-auction-request" + } + ] + } + ] + } + } + } + } + } + modules: + wurfl-devicedetection: + wurfl-file-dir-path: + wurfl-snapshot-url: https://data.scientiamobile.com//wurfl.zip + cache-size: 200000 + wurfl-run-updater: true + allowed-publisher-ids: 1 + ext-caps: false diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigPropertiesTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigPropertiesTest.java new file mode 100644 index 00000000000..dcce5123e34 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/config/WURFLDeviceDetectionConfigPropertiesTest.java @@ -0,0 +1,46 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class WURFLDeviceDetectionConfigPropertiesTest { + + @Test + void shouldInitializeWithEmptyValues() { + // given + final WURFLDeviceDetectionConfigProperties properties = new WURFLDeviceDetectionConfigProperties(); + + // then + assertThat(properties.getCacheSize()).isEqualTo(0); + assertThat(properties.getWurflFileDirPath()).isNull(); + assertThat(properties.getWurflSnapshotUrl()).isNull(); + assertThat(properties.isExtCaps()).isFalse(); + assertThat(properties.isWurflRunUpdater()).isTrue(); + } + + @Test + void shouldSetAndGetProperties() { + // given + final WURFLDeviceDetectionConfigProperties properties = new WURFLDeviceDetectionConfigProperties(); + + // when + properties.setCacheSize(1000); + properties.setWurflFileDirPath("/path/to/file"); + + properties.setWurflSnapshotUrl("https://example-scientiamobile.com/wurfl.zip"); + properties.setWurflRunUpdater(false); + properties.setAllowedPublisherIds(List.of("1", "3")); + properties.setExtCaps(true); + + // then + assertThat(properties.getCacheSize()).isEqualTo(1000); + assertThat(properties.getWurflFileDirPath()).isEqualTo("/path/to/file"); + assertThat(properties.getWurflSnapshotUrl()).isEqualTo("https://example-scientiamobile.com/wurfl.zip"); + assertThat(properties.isWurflRunUpdater()).isEqualTo(false); + assertThat(properties.getAllowedPublisherIds()).isEqualTo(List.of("1", "3")); + assertThat(properties.isExtCaps()).isTrue(); + } +} diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/mock/WURFLDeviceMock.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/mock/WURFLDeviceMock.java new file mode 100644 index 00000000000..a3280fe7d42 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/mock/WURFLDeviceMock.java @@ -0,0 +1,282 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock; + +import com.scientiamobile.wurfl.core.exc.CapabilityNotDefinedException; +import com.scientiamobile.wurfl.core.exc.VirtualCapabilityNotDefinedException; +import com.scientiamobile.wurfl.core.matchers.MatchType; +import lombok.Builder; + +import java.util.Map; + +@Builder +public class WURFLDeviceMock implements com.scientiamobile.wurfl.core.Device { + + private Map capabilities; + private String id; + private Map virtualCapabilities; + + @Override + public MatchType getMatchType() { + return MatchType.conclusive; + } + + @Override + public String getVirtualCapability(String vcapName) throws VirtualCapabilityNotDefinedException, + CapabilityNotDefinedException { + + if (!virtualCapabilities.containsKey(vcapName)) { + throw new VirtualCapabilityNotDefinedException(vcapName); + } + + return virtualCapabilities.get(vcapName); + } + + @Override + public int getVirtualCapabilityAsInt(String s) throws VirtualCapabilityNotDefinedException, + CapabilityNotDefinedException, NumberFormatException { + return 0; + } + + @Override + public boolean getVirtualCapabilityAsBool(String vcapName) throws VirtualCapabilityNotDefinedException, + CapabilityNotDefinedException, NumberFormatException { + + if (vcapName.equals("is_phone") || vcapName.equals("is_full_desktop") || vcapName.equals("is_connected_tv") + || vcapName.equals("is_mobile") || vcapName.equals("is_tablet")) { + return Boolean.parseBoolean(getVirtualCapability(vcapName)); + } + + return false; + } + + @Override + public Map getVirtualCapabilities() { + return Map.of(); + } + + @Override + public String getId() { + return id; + } + + @Override + public String getWURFLUserAgent() { + return ""; + } + + @Override + public String getCapability(String capName) throws CapabilityNotDefinedException { + + if (!capabilities.containsKey(capName)) { + throw new CapabilityNotDefinedException(capName); + } + + return capabilities.get(capName); + + } + + @Override + public int getCapabilityAsInt(String capName) throws CapabilityNotDefinedException, NumberFormatException { + return switch (capName) { + case "resolution_height", "resolution_width" -> Integer.parseInt(capabilities.get(capName)); + default -> 0; + }; + } + + @Override + public boolean getCapabilityAsBool(String capName) throws CapabilityNotDefinedException, NumberFormatException { + return switch (capName) { + case "ajax_support_javascript", "is_connected_tv", "is_ott", "is_tablet", "is_mobile" -> + Boolean.parseBoolean(getCapability(capName)); + default -> false; + }; + } + + @Override + public Map getCapabilities() { + return Map.of(); + } + + @Override + public boolean isActualDeviceRoot() { + return true; + } + + @Override + public String getDeviceRootId() { + return ""; + } + + public static class WURFLDeviceMockFactory { + + public static com.scientiamobile.wurfl.core.Device mockIPhone() { + + return builder().capabilities(Map.of( + "brand_name", "Apple", + "model_name", "iPhone", + "ajax_support_javascript", "true", + "density_class", "1.0", + "is_connected_tv", "false", + "is_ott", "false", + "is_tablet", "false", + "resolution_height", "1440", + "resolution_width", "3200" + )).virtualCapabilities( + Map.of("advertised_device_os", "iOS", + "advertised_device_os_version", "17.1", + "complete_device_name", "Apple iPhone", + "is_full_desktop", "false", + "is_mobile", "true", + "is_phone", "true", + "form_factor", "Smartphone", + "pixel_density", "515")) + .id("apple_iphone_ver1") + .build(); + } + + public static com.scientiamobile.wurfl.core.Device mockOttDevice() { + + return builder().capabilities(Map.of( + "brand_name", "Diyomate", + "model_name", "A6", + "ajax_support_javascript", "true", + "density_class", "1.5", + "is_connected_tv", "false", + "is_ott", "true", + "is_tablet", "false", + "resolution_height", "1080", + "resolution_width", "1920" + )).virtualCapabilities( + Map.of("advertised_device_os", "Android", + "advertised_device_os_version", "4.0", + "complete_device_name", "Diyomate A6", + "is_full_desktop", "false", + "is_mobile", "false", + "is_phone", "false", + "form_factor", "Smart-TV", + "pixel_density", "69")) + .id("diyomate_a6_ver1") + .build(); + } + + public static com.scientiamobile.wurfl.core.Device mockMobileUndefinedDevice() { + + return builder().capabilities(Map.of( + "brand_name", "TestUnd", + "model_name", "U1", + "ajax_support_javascript", "false", + "density_class", "1.0", + "is_connected_tv", "false", + "is_ott", "false", + "is_tablet", "false", + "resolution_height", "128", + "resolution_width", "128" + )).virtualCapabilities( + Map.of("advertised_device_os", "TestOS", + "advertised_device_os_version", "1.0", + "complete_device_name", "TestUnd U1", + "is_full_desktop", "false", + "is_mobile", "true", + "is_phone", "false", + "form_factor", "Test-non-phone", + "pixel_density", "69")) + .build(); + } + + public static com.scientiamobile.wurfl.core.Device mockUnknownDevice() { + + return builder().capabilities(Map.of( + "brand_name", "TestUnd", + "model_name", "U1", + "ajax_support_javascript", "false", + "density_class", "1.0", + "is_connected_tv", "false", + "is_ott", "false", + "is_tablet", "false", + "resolution_height", "128", + "resolution_width", "128" + )).virtualCapabilities( + Map.of("advertised_device_os", "TestOS", + "advertised_device_os_version", "1.0", + "complete_device_name", "TestUnd U1", + "is_full_desktop", "false", + "is_mobile", "false", + "is_phone", "false", + "form_factor", "Test-unknown", + "pixel_density", "69")) + .build(); + } + + public static com.scientiamobile.wurfl.core.Device mockDesktop() { + + return builder().capabilities(Map.of( + "brand_name", "TestDesktop", + "model_name", "D1", + "ajax_support_javascript", "true", + "density_class", "1.5", + "is_connected_tv", "false", + "is_ott", "false", + "is_tablet", "false", + "resolution_height", "1080", + "resolution_width", "1920" + )).virtualCapabilities( + Map.of("advertised_device_os", "Windows", + "advertised_device_os_version", "10", + "complete_device_name", "TestDesktop D1", + "is_full_desktop", "true", + "is_mobile", "false", + "is_phone", "false", + "form_factor", "Desktop", + "pixel_density", "300")) + .build(); + } + + public static com.scientiamobile.wurfl.core.Device mockConnectedTv() { + + return builder().capabilities(Map.of( + "brand_name", "TestConnectedTv", + "model_name", "C1", + "ajax_support_javascript", "true", + "density_class", "1.5", + "is_connected_tv", "true", + "is_ott", "false", + "is_tablet", "false", + "resolution_height", "1080", + "resolution_width", "1920" + )).virtualCapabilities( + Map.of("advertised_device_os", "WebOS", + "advertised_device_os_version", "4", + "complete_device_name", "TestConnectedTV C1", + "is_full_desktop", "false", + "is_mobile", "false", + "is_phone", "false", + "form_factor", "Smart-TV", + "pixel_density", "200")) + .build(); + } + + public static com.scientiamobile.wurfl.core.Device mockTablet() { + + return builder().capabilities(Map.of( + "brand_name", "Samsung", + "model_name", "Galaxy Tab S9+", + "ajax_support_javascript", "true", + "density_class", "1.5", + "is_connected_tv", "false", + "is_ott", "false", + "is_tablet", "true", + "resolution_height", "1752", + "resolution_width", "2800" + )).virtualCapabilities( + Map.of("advertised_device_os", "Android", + "advertised_device_os_version", "13", + "complete_device_name", "Samsung Galaxy Tab S9+", + "is_full_desktop", "false", + "is_mobile", "false", + "is_phone", "false", + "form_factor", "Tablet", + "pixel_density", "274")) + .build(); + } + + } +} diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContextTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContextTest.java new file mode 100644 index 00000000000..15a89de4ecf --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/AuctionRequestHeadersContextTest.java @@ -0,0 +1,65 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model; + +import org.junit.jupiter.api.Test; +import org.prebid.server.model.CaseInsensitiveMultiMap; + +import static org.assertj.core.api.Assertions.assertThat; + +class AuctionRequestHeadersContextTest { + + @Test + void fromShouldHandleNullHeaders() { + // when + final AuctionRequestHeadersContext result = AuctionRequestHeadersContext.from(null); + + // then + assertThat(result.headers).isEmpty(); + } + + @Test + void fromShouldConvertCaseInsensitiveMultiMapToHeaders() { + // given + final CaseInsensitiveMultiMap multiMap = CaseInsensitiveMultiMap.builder() + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test") + .add("Header2", "value2") + .build(); + + // when + final AuctionRequestHeadersContext target = AuctionRequestHeadersContext.from(multiMap); + + // then + assertThat(target.headers) + .hasSize(2) + .containsEntry("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test") + .containsEntry("Header2", "value2"); + } + + @Test + void fromShouldTakeFirstValueForDuplicateHeaders() { + // given + final CaseInsensitiveMultiMap multiMap = CaseInsensitiveMultiMap.builder() + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test") + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test2") + .build(); + + // when + final AuctionRequestHeadersContext target = AuctionRequestHeadersContext.from(multiMap); + + // then + assertThat(target.headers) + .hasSize(1) + .containsEntry("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test"); + } + + @Test + void fromShouldHandleEmptyMultiMap() { + // given + final CaseInsensitiveMultiMap emptyMultiMap = CaseInsensitiveMultiMap.empty(); + + // when + final AuctionRequestHeadersContext target = AuctionRequestHeadersContext.from(emptyMultiMap); + + // then + assertThat(target.headers).isEmpty(); + } +} diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializerTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializerTest.java new file mode 100644 index 00000000000..76d27053e75 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/model/WURFLEngineInitializerTest.java @@ -0,0 +1,125 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model; + +import com.scientiamobile.wurfl.core.GeneralWURFLEngine; +import com.scientiamobile.wurfl.core.WURFLEngine; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.exc.WURFLModuleConfigurationException; +import org.junit.jupiter.api.function.Executable; + +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mockStatic; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class WURFLEngineInitializerTest { + + @Mock(strictness = LENIENT) + private WURFLDeviceDetectionConfigProperties configProperties; + + @Mock(strictness = LENIENT) + private WURFLEngine wurflEngine; + + @BeforeEach + void setUp() { + when(configProperties.getWurflSnapshotUrl()).thenReturn("http://test.url/wurfl.zip"); + when(configProperties.getWurflFileDirPath()).thenReturn("/test/path"); + } + + @Test + void downloadWurflFileIfNeededShouldDownloadWhenUrlAndPathArePresent() { + try (MockedStatic mockedStatic = mockStatic(GeneralWURFLEngine.class)) { + // when + WURFLEngineInitializer.downloadWurflFile(configProperties); + + // then + mockedStatic.verify(() -> + GeneralWURFLEngine.wurflDownload("http://test.url/wurfl.zip", "/test/path")); + } + } + + @Test + void verifyStaticCapabilitiesDefinitionShouldThrowExceptionWhenCapabilitiesAreNotDefined() { + // given + when(wurflEngine.getAllCapabilities()).thenReturn(Set.of( + "brand_name", + "density_class", + "is_connected_tv", + "is_ott", + "is_tablet", + "model_name")); + + final String expFailedCheckMessage = """ + Static capabilities %s needed for device enrichment are not defined in WURFL. + Please make sure that your license has the needed capabilities or upgrade it. + """.formatted(String.join(",", List.of( + "ajax_support_javascript", + "physical_form_factor", + "resolution_height", + "resolution_width" + ))); + + // when + final Executable exceptionSource = () -> WURFLEngineInitializer.verifyStaticCapabilitiesDefinition(wurflEngine); + + // then + final Exception exception = assertThrows(WURFLModuleConfigurationException.class, exceptionSource); + assertThat(exception.getMessage()).isEqualTo(expFailedCheckMessage); + } + + @Test + void verifyStaticCapabilitiesDefinitionShouldCompleteSuccessfullyWhenCapabilitiesAreDefined() { + // given + when(wurflEngine.getAllCapabilities()).thenReturn(Set.of( + "brand_name", + "density_class", + "is_connected_tv", + "is_ott", + "is_tablet", + "model_name", + "resolution_width", + "resolution_height", + "physical_form_factor", + "ajax_support_javascript" + )); + + // when + var excOccurred = false; + try { + WURFLEngineInitializer.verifyStaticCapabilitiesDefinition(wurflEngine); + } catch (Exception e) { + excOccurred = true; + } + + // then + assertThat(excOccurred).isFalse(); + } + + @Test + void builderShouldCreateWURFLEngineInitializerBuilderFromProperties() { + // given + when(configProperties.getWurflSnapshotUrl()).thenReturn("http://test.url/wurfl.zip"); + when(configProperties.getWurflFileDirPath()).thenReturn("/test/path"); + when(configProperties.getCacheSize()).thenReturn(1000); + when(configProperties.isWurflRunUpdater()).thenReturn(true); + + // when + final var builder = WURFLEngineInitializer.builder() + .configProperties(configProperties); + + // then + assertThat(builder).isNotNull(); + assertThat(builder.build()).isNotNull(); + assertThat(builder.toString()).isNotEmpty(); + } +} diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolverTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolverTest.java new file mode 100644 index 00000000000..f3d3be006fa --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/HeadersResolverTest.java @@ -0,0 +1,199 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver; + +import com.iab.openrtb.request.BrandVersion; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.UserAgent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class HeadersResolverTest { + + private HeadersResolver target; + + private static final String TEST_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"; + + @BeforeEach + void setUp() { + target = new HeadersResolver(); + } + + @Test + void resolveWithNullDeviceShouldReturnOriginalHeaders() { + // given + final Map headers = new HashMap<>(); + headers.put("test", "value"); + + // when + final Map result = target.resolve(null, headers); + + // then + assertThat(result).isEqualTo(headers); + } + + @Test + void resolveWithDeviceUaShouldReturnUserAgentHeader() { + // given + final Device device = Device.builder() + .ua("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36") + .build(); + + // when + final Map result = target.resolve(device, new HashMap<>()); + + // then + assertThat(result).containsEntry("User-Agent", TEST_USER_AGENT); + } + + @Test + void resolveWithFullSuaShouldReturnAllHeaders() { + // given + final BrandVersion brandVersion = new BrandVersion( + "Chrome", + Arrays.asList("100", "0", "0"), + null); + + final BrandVersion winBrandVersion = new BrandVersion( + "Windows", + Arrays.asList("10", "0", "0"), + null); + final UserAgent sua = UserAgent.builder() + .browsers(List.of(brandVersion)) + .platform(winBrandVersion) + .model("Test Model") + .architecture("x86") + .mobile(0) + .build(); + + final Device device = Device.builder() + .sua(sua) + .build(); + + // when + final Map result = target.resolve(device, new HashMap<>()); + + // then + assertThat(result) + .containsEntry("Sec-CH-UA", "\"Chrome\";v=\"100.0.0\"") + .containsEntry("Sec-CH-UA-Full-Version-List", "\"Chrome\";v=\"100.0.0\"") + .containsEntry("Sec-CH-UA-Platform", "\"Windows\"") + .containsEntry("Sec-CH-UA-Platform-Version", "\"10.0.0\"") + .containsEntry("Sec-CH-UA-Model", "\"Test Model\"") + .containsEntry("Sec-CH-UA-Arch", "\"x86\"") + .containsEntry("Sec-CH-UA-Mobile", "?0"); + } + + @Test + void resolveWithFullDeviceAndHeadersShouldPrioritizeDevice() { + // given + final BrandVersion brandVersion = new BrandVersion( + "Chrome", + Arrays.asList("100", "0", "0"), + null); + + final BrandVersion winBrandVersion = new BrandVersion( + "Windows", + Arrays.asList("10", "0", "0"), + null); + final UserAgent sua = UserAgent.builder() + .browsers(List.of(brandVersion)) + .platform(winBrandVersion) + .model("Test Model") + .architecture("x86") + .mobile(0) + .build(); + + final Device device = Device.builder() + .sua(sua) + .ua(TEST_USER_AGENT) + .build(); + + final Map headers = new HashMap<>(); + headers.put("Sec-CH-UA", "Test UA-CH"); + headers.put("Sec-CH-UA-Full-Version-List", "Test-UA-Full-Version-List"); + headers.put("Sec-CH-UA-Platform", "Test-UA-Platform"); + headers.put("Sec-CH-UA-Platform-Version", "Test-UA-Platform-Version"); + headers.put("Sec-CH-UA-Model", "Test-UA-Model"); + headers.put("Sec-CH-UA-Arch", "Test-UA-Arch"); + headers.put("Sec-CH-UA-Mobile", "Test-UA-Mobile"); + headers.put("User-Agent", "Mozilla/5.0 (Test OS; 10) like Gecko"); + // when + final Map result = target.resolve(device, headers); + + // then + assertThat(result) + .containsEntry("Sec-CH-UA", "\"Chrome\";v=\"100.0.0\"") + .containsEntry("Sec-CH-UA-Full-Version-List", "\"Chrome\";v=\"100.0.0\"") + .containsEntry("Sec-CH-UA-Platform", "\"Windows\"") + .containsEntry("Sec-CH-UA-Platform-Version", "\"10.0.0\"") + .containsEntry("Sec-CH-UA-Model", "\"Test Model\"") + .containsEntry("Sec-CH-UA-Arch", "\"x86\"") + .containsEntry("Sec-CH-UA-Mobile", "?0"); + } + + @Test + void versionFromTokensShouldHandleNullAndEmptyInput() { + // when & then + assertThat(HeadersResolver.versionFromTokens(null)).isEmpty(); + assertThat(HeadersResolver.versionFromTokens(List.of())).isEmpty(); + } + + @Test + void versionFromTokensShouldJoinValidTokens() { + // given + final List tokens = Arrays.asList("100", "0", "1234"); + + // when + final String result = HeadersResolver.versionFromTokens(tokens); + + // then + assertThat(result).isEqualTo("100.0.1234"); + } + + @Test + void resolveWithMultipleBrandVersionsShouldFormatCorrectly() { + // given + final BrandVersion chrome = new BrandVersion("Chrome", + Arrays.asList("100", "0"), + null); + final BrandVersion chromium = new BrandVersion("Chromium", + Arrays.asList("100", "0"), + null); + + final BrandVersion notABrand = new BrandVersion("Not\\A;Brand", + Arrays.asList("99", "0"), + null); + + final UserAgent sua = UserAgent.builder() + .browsers(Arrays.asList(chrome, chromium, notABrand)) + .build(); + + final Device device = Device.builder() + .sua(sua) + .build(); + + // when + final Map result = target.resolve(device, new HashMap<>()); + + // then + final String expectedFormat = "\"Chrome\";v=\"100.0\", \"Chromium\";v=\"100.0\", \"Not\\A;Brand\";v=\"99.0\""; + assertThat(result) + .containsEntry("Sec-CH-UA", expectedFormat) + .containsEntry("Sec-CH-UA-Full-Version-List", expectedFormat); + } + + @Test + void resolveWithNullDeviceAndNullHeadersShouldReturnEmptyMap() { + // when + final Map result = target.resolve(null, null); + + // then + assertThat(result).isEmpty(); + } +} diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersionTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersionTest.java new file mode 100644 index 00000000000..666e4571908 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/resolver/PlatformNameVersionTest.java @@ -0,0 +1,69 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.resolver; + +import com.iab.openrtb.request.BrandVersion; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class PlatformNameVersionTest { + + @Test + void fromShouldReturnNullWhenPlatformIsNull() { + // when + final PlatformNameVersion target = PlatformNameVersion.from(null); + + // then + assertThat(target).isNull(); + } + + @Test + void fromShouldCreatePlatformNameVersionWithValidInput() { + // given + final BrandVersion platform = new BrandVersion("Windows", + Arrays.asList("10", "0", "0"), + null); + + // when + final PlatformNameVersion target = PlatformNameVersion.from(platform); + + // then + assertThat(target).isNotNull(); + assertThat(target.getPlatformName()).isEqualTo("Windows"); + assertThat(target.getPlatformVersion()).isEqualTo("10.0.0"); + } + + @Test + void toStringShouldReturnFormattedString() { + // given + final BrandVersion platform = new BrandVersion("macOS", + Arrays.asList("13", "1"), + null); + final PlatformNameVersion target = PlatformNameVersion.from(platform); + + // when + final String result = target.toString(); + + // then + assertThat(result).isEqualTo("macOS 13.1"); + } + + @Test + void fromShouldHandleEmptyVersionList() { + // given + final BrandVersion platform = new BrandVersion("Linux", + List.of(), + null); + + // when + final PlatformNameVersion target = PlatformNameVersion.from(platform); + + // then + assertThat(target).isNotNull(); + assertThat(target.getPlatformName()).isEqualTo("Linux"); + assertThat(target.getPlatformVersion()).isEmpty(); + assertThat(target.toString()).isEqualTo("Linux "); + } +} diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidatorTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidatorTest.java new file mode 100644 index 00000000000..a21e2fca77b --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/AccountValidatorTest.java @@ -0,0 +1,110 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.settings.model.Account; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AccountValidatorTest { + + private AuctionContext auctionContext; + + @Mock + private Account account; + + private AccountValidator validator; + + @BeforeEach + void setUp() { + auctionContext = AuctionContext.builder().account(account).build(); + } + + @Test + void isAccountValidShouldReturnTrueWhenPublisherIdIsAllowed() { + // given + when(account.getId()).thenReturn("allowed-publisher"); + final var accountValidatorBuiler = AccountValidator.builder() + .allowedPublisherIds(Collections.singletonMap("allowed-publisher", "allowed-publisher")) + .auctionContext(auctionContext); + assertThat(accountValidatorBuiler.toString()).isNotNull(); + validator = accountValidatorBuiler.build(); + + // when + final boolean result = validator.isAccountValid(); + + // then + assertThat(result).isTrue(); + } + + @Test + void isAccountValidShouldReturnFalseWhenPublisherIdIsNotAllowed() { + // given + when(account.getId()).thenReturn("unknown-publisher"); + validator = AccountValidator.builder() + .allowedPublisherIds(Collections.singletonMap("allowed-publisher", "allowed-publisher")) + .auctionContext(auctionContext) + .build(); + + // when + final boolean result = validator.isAccountValid(); + + // then + assertThat(result).isFalse(); + } + + @Test + void isAccountValidShouldReturnFalseWhenAuctionContextIsNull() { + // given + validator = AccountValidator.builder() + .allowedPublisherIds(Collections.singletonMap("allowed-publisher", "allowed-publisher")) + .auctionContext(null) + .build(); + + // when + final boolean result = validator.isAccountValid(); + + // then + assertThat(result).isFalse(); + } + + @Test + void isAccountValidShouldReturnFalseWhenPublisherIdIsEmpty() { + // given + when(account.getId()).thenReturn(""); + validator = AccountValidator.builder() + .allowedPublisherIds(Collections.singletonMap("allowed-publisher", "allowed-publisher")) + .auctionContext(auctionContext) + .build(); + + // when + final boolean result = validator.isAccountValid(); + + // then + assertThat(result).isFalse(); + } + + @Test + void isAccountValidShouldReturnFalseWhenAccountIsNull() { + // given + when(auctionContext.getAccount()).thenReturn(null); + validator = AccountValidator.builder() + .allowedPublisherIds(Collections.singletonMap("allowed-publisher", "allowed-publisher")) + .auctionContext(auctionContext) + .build(); + + // when + final boolean result = validator.isAccountValid(); + + // then + assertThat(result).isFalse(); + } +} diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapperTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapperTest.java new file mode 100644 index 00000000000..8e717fcf1ce --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/ExtWURFLMapperTest.java @@ -0,0 +1,131 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.Device; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ExtWURFLMapperTest { + + @Mock + private com.scientiamobile.wurfl.core.Device wurflDevice; + + @Mock + private Device device; + + private ExtWURFLMapper target; + private List staticCaps; + private List virtualCaps; + + @BeforeEach + public void setUp() { + staticCaps = Arrays.asList("brand_name", "model_name"); + virtualCaps = Arrays.asList("is_mobile", "form_factor"); + + target = ExtWURFLMapper.builder() + .staticCaps(staticCaps) + .virtualCaps(virtualCaps) + .wurflDevice(wurflDevice) + .addExtCaps(true) + .build(); + } + + @Test + public void shouldMapStaticCapabilities() { + // given + when(wurflDevice.getCapability("brand_name")).thenReturn("Apple"); + when(wurflDevice.getCapability("model_name")).thenReturn("iPhone"); + + // when + final JsonNode result = target.mapExtProperties(); + + // then + assertThat(result.get("brand_name").asText()).isEqualTo("Apple"); + assertThat(result.get("model_name").asText()).isEqualTo("iPhone"); + } + + @Test + public void shouldMapVirtualCapabilities() { + // given + when(wurflDevice.getVirtualCapability("is_mobile")).thenReturn("true"); + when(wurflDevice.getVirtualCapability("form_factor")).thenReturn("smartphone"); + + // when + final JsonNode result = target.mapExtProperties(); + + // then + assertThat(result.get("is_mobile").asText()).isEqualTo("true"); + assertThat(result.get("form_factor").asText()).isEqualTo("smartphone"); + } + + @Test + public void shouldMapWURFLId() { + // given + when(wurflDevice.getId()).thenReturn("test_wurfl_id"); + + // when + final JsonNode result = target.mapExtProperties(); + + // then + assertThat(result.get("wurfl_id").asText()).isEqualTo("test_wurfl_id"); + } + + @Test + public void shouldSkipNullCapabilities() { + // given + when(wurflDevice.getCapability("brand_name")).thenReturn(null); + when(wurflDevice.getCapability("model_name")).thenReturn("iPhone"); + when(wurflDevice.getVirtualCapability("is_mobile")).thenReturn(null); + + // when + final JsonNode result = target.mapExtProperties(); + + // then + assertThat(result.has("brand_name")).isFalse(); + assertThat(result.get("model_name").asText()).isEqualTo("iPhone"); + assertThat(result.has("is_mobile")).isFalse(); + } + + @Test + public void shouldHandleExceptionsGracefully() { + // given + when(wurflDevice.getCapability("brand_name")).thenThrow(new RuntimeException("Test exception")); + when(wurflDevice.getCapability("model_name")).thenReturn("iPhone"); + + // when + final JsonNode result = target.mapExtProperties(); + + // then + assertThat(result.has("brand_name")).isFalse(); + assertThat(result.get("model_name")).isNotNull(); + assertThat(result.get("model_name").asText()).isEqualTo("iPhone"); + } + + @Test + public void shouldNotAddExtCapsIfDisabled() { + // given + target = ExtWURFLMapper.builder() + .staticCaps(staticCaps) + .virtualCaps(virtualCaps) + .wurflDevice(wurflDevice) + .addExtCaps(false) + .build(); + + // when + final JsonNode result = target.mapExtProperties(); + + // then + assertThat(result.has("brand_name")).isFalse(); + assertThat(result.has("model_name")).isFalse(); + } +} 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 new file mode 100644 index 00000000000..f0f60f43383 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/OrtbDeviceUpdaterTest.java @@ -0,0 +1,243 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.Device; +import org.prebid.server.proto.openrtb.ext.request.ExtDevice; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockConnectedTv; +import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockDesktop; +import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockIPhone; +import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockMobileUndefinedDevice; +import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockOttDevice; +import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockTablet; +import static org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock.WURFLDeviceMockFactory.mockUnknownDevice; + +@Slf4j +@ExtendWith(MockitoExtension.class) +class OrtbDeviceUpdaterTest { + + private OrtbDeviceUpdater target; + private List staticCaps; + private List virtualCaps; + + @BeforeEach + void setUp() { + target = new OrtbDeviceUpdater(); + staticCaps = Arrays.asList("ajax_support_javascript", "brand_name", "density_class", + "is_connected_tv", "is_ott", "is_tablet", "model_name", "resolution_height", "resolution_width"); + virtualCaps = Arrays.asList("advertised_device_os", "advertised_device_os_version", + "is_full_desktop", "pixel_density"); + } + + @Test + void updateShouldUpdateDeviceMakeWhenOriginalIsEmpty() { + // given + final var wurflDevice = mockIPhone(); + final Device device = Device.builder().build(); + + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + + // then + assertThat(result.getMake()).isEqualTo("Apple"); + assertThat(result.getDevicetype()).isEqualTo(1); + } + + @Test + void updateShouldNotUpdateDeviceMakeWhenOriginalExists() { + // given + final Device device = Device.builder().make("Samsung").build(); + final var wurflDevice = mockIPhone(); + + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + + // then + assertThat(result.getMake()).isEqualTo("Samsung"); + } + + @Test + void updateShouldNotUpdateDeviceMakeWhenOriginalBigIntegerExists() { + // given + final Device device = Device.builder().make("Apple").pxratio(new BigDecimal("1.0")).build(); + final var wurflDevice = mockIPhone(); + + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + + // then + assertThat(result.getMake()).isEqualTo("Apple"); + assertThat(result.getPxratio()).isEqualTo("1.0"); + } + + @Test + void updateShouldUpdateDeviceModelWhenOriginalIsEmpty() { + // given + final Device device = Device.builder().build(); + final var wurflDevice = mockIPhone(); + + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + + // then + assertThat(result.getModel()).isEqualTo("iPhone"); + } + + @Test + void updateShouldUpdateDeviceOsWhenOriginalIsEmpty() { + // given + final Device device = Device.builder().build(); + final var wurflDevice = mockIPhone(); + + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + + // then + assertThat(result.getOs()).isEqualTo("iOS"); + } + + @Test + void updateShouldUpdateResolutionWhenOriginalIsEmpty() { + // given + final Device device = Device.builder().build(); + final var wurflDevice = mockIPhone(); + + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + + // then + assertThat(result.getW()).isEqualTo(3200); + assertThat(result.getH()).isEqualTo(1440); + } + + @Test + void updateShouldHandleJavascriptSupport() { + // given + final Device device = Device.builder().build(); + final var wurflDevice = mockIPhone(); + + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + + // then + assertThat(result.getJs()).isEqualTo(1); + } + + @Test + void updateShouldHandleOttDeviceType() { + // given + final Device device = Device.builder().build(); + final var wurflDevice = mockOttDevice(); + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + + // then + assertThat(result.getDevicetype()).isEqualTo(7); + } + + @Test + void updateShouldReturnDeviceOtherMobileWhenMobileIsNotPhoneOrTablet() { + // given + final Device device = Device.builder().build(); + final var wurflDevice = mockMobileUndefinedDevice(); + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + // then + assertThat(result.getDevicetype()).isEqualTo(6); + } + + @Test + void updateShouldReturnNullWhenMobileTypeIsUnknown() { + // given + final Device device = Device.builder().build(); + final var wurflDevice = mockUnknownDevice(); + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + // then + assertThat(result.getDevicetype()).isNull(); + } + + @Test + void updateShouldHandlePersonalComputerDeviceType() { + // given + final Device device = Device.builder().build(); + final var wurflDevice = mockDesktop(); + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + // then + assertThat(result.getDevicetype()).isEqualTo(2); + } + + @Test + void updateShouldHandleConnectedTvDeviceType() { + // given + final Device device = Device.builder().build(); + final var wurflDevice = mockConnectedTv(); + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + // then + assertThat(result.getDevicetype()).isEqualTo(3); + } + + @Test + void updateShouldNotUpdateDeviceTypeWhenSet() { + // given + final Device device = Device.builder() + .devicetype(3) + .build(); + final var wurflDevice = mockDesktop(); // device type 2 + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + // then + assertThat(result.getDevicetype()).isEqualTo(3); // unchanged + } + + @Test + void updateShouldHandleTabletDeviceType() { + // given + final Device device = Device.builder().build(); + final var wurflDevice = mockTablet(); + // when + final Device result = target.update(device, wurflDevice, staticCaps, virtualCaps, true); + // then + assertThat(result.getDevicetype()).isEqualTo(5); + } + + @Test + void updateShouldAddWurflPropertyToExtIfMissingAndPreserveExistingProperties() { + // given + final ExtDevice existingExt = ExtDevice.empty(); + existingExt.addProperty("someProperty", TextNode.valueOf("value")); + final Device device = Device.builder() + .ext(existingExt) + .build(); + + final var wurflDevice = WURFLDeviceMock.WURFLDeviceMockFactory.mockIPhone(); + final List staticCaps = List.of("brand_name"); + final List virtualCaps = List.of("advertised_device_os"); + + final OrtbDeviceUpdater updater = new OrtbDeviceUpdater(); + + // when + final Device result = updater.update(device, wurflDevice, staticCaps, virtualCaps, true); + + // then + final ExtDevice resultExt = result.getExt(); + assertThat(resultExt).isNotNull(); + assertThat(resultExt.getProperty("someProperty").textValue()).isEqualTo("value"); + assertThat(resultExt.getProperty("wurfl")).isNotNull(); + + } + +} diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHookTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHookTest.java new file mode 100644 index 00000000000..b648015637e --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionEntrypointHookTest.java @@ -0,0 +1,81 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; +import org.prebid.server.model.CaseInsensitiveMultiMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class WURFLDeviceDetectionEntrypointHookTest { + + private EntrypointPayload payload; + private InvocationContext context; + + @BeforeEach + void setUp() { + payload = mock(EntrypointPayload.class); + context = mock(InvocationContext.class); + } + + @Test + void codeShouldReturnCorrectHookCode() { + + // given + final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook(); + + // when + final String result = target.code(); + + // then + assertThat(result).isEqualTo("wurfl-devicedetection-entrypoint-hook"); + } + + @Test + void callShouldReturnSuccessWithNoAction() { + // given + final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook(); + final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder() + .add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Test") + .build(); + when(payload.headers()).thenReturn(headers); + + // when + final Future> result = target.call(payload, context); + + // then + assertThat(result).isNotNull(); + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + assertThat(invocationResult.moduleContext()).isNotNull(); + } + + @Test + void callShouldHandleNullHeaders() { + // given + final WURFLDeviceDetectionEntrypointHook target = new WURFLDeviceDetectionEntrypointHook(); + + // when + when(payload.headers()).thenReturn(null); + final Future> result = target.call(payload, context); + + // then + assertThat(result).isNotNull(); + assertThat(result.succeeded()).isTrue(); + final InvocationResult invocationResult = result.result(); + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action); + assertThat(invocationResult.moduleContext()).isNotNull(); + assertThat(invocationResult.moduleContext() instanceof AuctionRequestHeadersContext).isTrue(); + } +} diff --git a/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModuleTest.java b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModuleTest.java new file mode 100644 index 00000000000..b2d36390f52 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionModuleTest.java @@ -0,0 +1,40 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; + +class WURFLDeviceDetectionModuleTest { + + @Test + void codeShouldReturnCorrectModuleCode() { + // given + final List> hooks = new ArrayList<>(); + final WURFLDeviceDetectionModule target = new WURFLDeviceDetectionModule(hooks); + + // when + final String result = target.code(); + + // then + assertThat(result).isEqualTo("wurfl-devicedetection"); + } + + @Test + void hooksShouldReturnProvidedHooks() { + // given + final List> hooks = new ArrayList<>(); + final WURFLDeviceDetectionModule target = new WURFLDeviceDetectionModule(hooks); + + // when + final Collection> result = target.hooks(); + + // then + assertThat(result).isSameAs(hooks); + } +} 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 new file mode 100644 index 00000000000..5b5a1d01f74 --- /dev/null +++ b/extra/modules/WURFL-devicedetection/src/test/java/org/prebid/server/hooks/modules/com/scientiamobile/wurfl/devicedetection/v1/WURFLDeviceDetectionRawAuctionRequestHookTest.java @@ -0,0 +1,124 @@ +package org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.v1; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.scientiamobile.wurfl.core.WURFLEngine; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.config.WURFLDeviceDetectionConfigProperties; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.mock.WURFLDeviceMock; +import org.prebid.server.hooks.modules.com.scientiamobile.wurfl.devicedetection.model.AuctionRequestHeadersContext; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.model.CaseInsensitiveMultiMap; + +import java.util.Collections; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class WURFLDeviceDetectionRawAuctionRequestHookTest { + + @Mock + private WURFLEngine wurflEngine; + + @Mock + private WURFLDeviceDetectionConfigProperties configProperties; + + @Mock + private AuctionRequestPayload payload; + + @Mock + private AuctionInvocationContext context; + + private WURFLDeviceDetectionRawAuctionRequestHook target; + + @BeforeEach + void setUp() { + + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflEngine, configProperties); + } + + @Test + void codeShouldReturnCorrectHookCode() { + // when + final String result = target.code(); + + // then + assertThat(result).isEqualTo("wurfl-devicedetection-raw-auction-request"); + } + + @Test + void callShouldReturnNoActionWhenBidRequestIsNull() { + // given + when(payload.bidRequest()).thenReturn(null); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + void callShouldReturnNoActionWhenDeviceIsNull() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + when(payload.bidRequest()).thenReturn(bidRequest); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + void callShouldUpdateDeviceWhenWurflDeviceIsDetected() { + // given + final String ua = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_7_2) Version/17.4.1 Mobile/15E148 Safari/604.1"; + final Device device = Device.builder().ua(ua).build(); + final BidRequest bidRequest = BidRequest.builder().device(device).build(); + when(payload.bidRequest()).thenReturn(bidRequest); + + final CaseInsensitiveMultiMap headers = CaseInsensitiveMultiMap.builder() + .add("User-Agent", ua) + .build(); + final AuctionRequestHeadersContext headersContext = AuctionRequestHeadersContext.from(headers); + + // when + when(context.moduleContext()).thenReturn(headersContext); + final var wurflDevice = WURFLDeviceMock.WURFLDeviceMockFactory.mockIPhone(); + when(wurflEngine.getDeviceForRequest(any(Map.class))).thenReturn(wurflDevice); + + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + } + + @Test + void shouldEnrichDeviceWhenAllowedPublisherIdsIsEmpty() { + // given + when(configProperties.getAllowedPublisherIds()).thenReturn(Collections.emptyList()); + target = new WURFLDeviceDetectionRawAuctionRequestHook(wurflEngine, configProperties); + + // when + final InvocationResult result = target.call(payload, context).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + } +} diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index d6bdcf20c34..c16e5072d26 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -24,6 +24,7 @@ pb-response-correction greenbids-real-time-data pb-request-correction + diff --git a/sample/configs/prebid-config-with-wurfl.yaml b/sample/configs/prebid-config-with-wurfl.yaml new file mode 100644 index 00000000000..c9f6b1d63d2 --- /dev/null +++ b/sample/configs/prebid-config-with-wurfl.yaml @@ -0,0 +1,80 @@ +status-response: "ok" +adapters: + appnexus: + enabled: true + ix: + enabled: true + openx: + enabled: true + pubmatic: + enabled: true + rubicon: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + filesystem: + settings-filename: sample/configs/sample-app-settings.yaml + stored-requests-dir: sample + stored-imps-dir: sample + stored-responses-dir: sample + categories-dir: +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false +hooks: + wurfl-devicedetection: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "entrypoint": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "wurfl-devicedetection", + "hook_impl_code": "wurfl-devicedetection-entrypoint-hook" + } + ] + } + ] + }, + "raw_auction_request": { + "groups": [ + { + "timeout": 10, + "hook_sequence": [ + { + "module_code": "wurfl-devicedetection", + "hook_impl_code": "wurfl-devicedetection-raw-auction-request" + } + ] + } + ] + } + } + } + } + } + From eef4efc3b8692cfcb01796415bf4f792763d67a1 Mon Sep 17 00:00:00 2001 From: andrea Date: Fri, 17 Jan 2025 10:48:39 +0100 Subject: [PATCH 2/8] updated README.md --- extra/modules/WURFL-devicedetection/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extra/modules/WURFL-devicedetection/README.md b/extra/modules/WURFL-devicedetection/README.md index 86eb9cd8f6b..f25218e6c7e 100644 --- a/extra/modules/WURFL-devicedetection/README.md +++ b/extra/modules/WURFL-devicedetection/README.md @@ -1,5 +1,7 @@ ## WURFL-devicedetection module +### Overview + The **WURFL Device Enrichment Module** for Prebid Server enhances the OpenRTB 2.x payload with comprehensive device detection data powered by **ScientiaMobile**’s WURFL device detection framework. Thanks to WURFL's device database, the module provides accurate and comprehensive device-related information, From 12eb97cc759244d49dc6c0ff8b7b4de89d91df8d Mon Sep 17 00:00:00 2001 From: andrea Date: Fri, 17 Jan 2025 10:58:59 +0100 Subject: [PATCH 3/8] updated README.md title --- extra/modules/WURFL-devicedetection/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/WURFL-devicedetection/README.md b/extra/modules/WURFL-devicedetection/README.md index f25218e6c7e..cf9a9123fed 100644 --- a/extra/modules/WURFL-devicedetection/README.md +++ b/extra/modules/WURFL-devicedetection/README.md @@ -1,4 +1,4 @@ -## WURFL-devicedetection module +## WURFL Device Enrichment Module ### Overview From da8fd51c5433461936e2db3605716727d1dc1a63 Mon Sep 17 00:00:00 2001 From: andrea Date: Fri, 17 Jan 2025 11:40:38 +0100 Subject: [PATCH 4/8] some README.md wording improvements --- extra/modules/WURFL-devicedetection/README.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/extra/modules/WURFL-devicedetection/README.md b/extra/modules/WURFL-devicedetection/README.md index cf9a9123fed..108fd495963 100644 --- a/extra/modules/WURFL-devicedetection/README.md +++ b/extra/modules/WURFL-devicedetection/README.md @@ -11,7 +11,7 @@ enabling bidders to make better-informed targeting and optimization decisions. #### Device Field Enrichment: -The module populates missing or empty fields in ortb2.device with the following data: +The WURFL module populates missing or empty fields in ortb2.device with the following data: - **make**: Manufacturer of the device (e.g., "Apple", "Samsung"). - **model**: Device model (e.g., "iPhone 14", "Galaxy S22"). - **os**: Operating system (e.g., "iOS", "Android"). @@ -30,10 +30,10 @@ The module identifies publishers through the `getAccount()` method in the `Aucti ### Build prerequisites -To build the WURFL device detection module, you need to download the WURFL Onsite Java API (both JAR and POM files) -from the Scientiamobile private repository and install it in your local Maven repository. -Access to the WURFL Onsite Java API repository requires a valid Scientiamobile WURFL license. -For more details, visit: [Scientiamobile WURFL Onsite API for Java](https://www.scientiamobile.com/secondary-products/wurfl-onsite-api-for-java/). +To build the WURFL module, you need to download the WURFL Onsite Java API (both JAR and POM files) +from the ScientiaMobile private repository and install it in your local Maven repository. +Access to the WURFL Onsite Java API repository requires a valid ScientiaMobile WURFL license. +For more details, visit: [ScientiaMobile WURFL Onsite API for Java](https://www.scientiamobile.com/secondary-products/wurfl-onsite-api-for-java/). Run the following command to install the WURFL API: @@ -47,12 +47,12 @@ mvn install:install-file \ -DpomFile= ``` -### Activating the WURFL Device Detection Module +### Activating the WURFL Module -The WURFL device detection module is disabled by default. Building the Prebid Server Java with the default bundle option +The WURFL module is disabled by default. Building the Prebid Server Java with the default bundle option does not include the WURFL module in the server's JAR file. -To include the WURFL device detection module in the Prebid Server Java bundle, follow these steps: +To include the WURFL module in the Prebid Server Java bundle, follow these steps: 1. Uncomment the WURFL Java API dependency in `extra/modules/WURFL-devicedetection/pom.xml`. 2. Uncomment the WURFL module dependency in `extra/bundle/pom.xml`. @@ -64,9 +64,9 @@ After making these changes, you can build the Prebid Server Java bundle with the mvn clean package --file extra/pom.xml ``` -### Configuring the WURFL Device Detection Module +### Configuring the WURFL Module -Below is a sample configuration for the WURFL device detection module: +Below is a sample configuration for the WURFL module: ```yaml hooks: From c0dd07765e686ed73f3bf7492b0256ff0ff99939 Mon Sep 17 00:00:00 2001 From: andrea Date: Fri, 17 Jan 2025 11:56:09 +0100 Subject: [PATCH 5/8] used markdown table for config params. --- extra/modules/WURFL-devicedetection/README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/extra/modules/WURFL-devicedetection/README.md b/extra/modules/WURFL-devicedetection/README.md index 108fd495963..c50ee88595f 100644 --- a/extra/modules/WURFL-devicedetection/README.md +++ b/extra/modules/WURFL-devicedetection/README.md @@ -119,13 +119,15 @@ hooks: ### Configuration Options -- **`wurfl-file-dir-path`** (Mandatory): Path to the directory where the WURFL file is downloaded. Directory must exist and be writable. -- **`wurfl-file-name`** (Mandatory): Name of the WURFL file, typically `wurfl.zip`. -- **`wurfl-file-url`** (Mandatory): URL to the licensed WURFL file to be downloaded when Prebid Server Java starts. -- **`cache-size`** (Optional): Maximum number of devices stored in the WURFL cache. Defaults to the WURFL cache's standard size. -- **`ext_caps`** (Optional): If `true`, the module adds all licensed capabilities to the `device.ext` object. -- **`wurfl-updater-frequency`** (Optional): Frequency for updating the WURFL file. Defaults to no updates. -- **`allowed_publisher_ids`** (Optional): List of publisher IDs permitted to use the module. Defaults to all publishers. +| Parameter | Requirement | Description | +|---------------------------|-------------|-------------------------------------------------------------------------------------------------------| +| **`wurfl-file-dir-path`** | Mandatory | Path to the directory where the WURFL file is downloaded. Directory must exist and be writable. | +| **`wurfl-snapshot-url`** | Mandatory | URL of the licensed WURFL snapshot file to be downloaded when Prebid Server Java starts. | +| **`cache-size`** | Optional | Maximum number of devices stored in the WURFL cache. Defaults to the WURFL cache's standard size. | +| **`ext-caps`** | Optional | If `true`, the module adds all licensed capabilities to the `device.ext` object. | +| **`wurfl-run-updater`** | Optional | Enables the WURFL updater. Defaults to no updates. | +| **`allowed-publisher-ids`** | Optional | List of publisher IDs permitted to use the module. Defaults to all publishers. | + A valid WURFL license must include all the required capabilities for device enrichment. From 39bf6d710dd598473ea757b3b55119f34c999bf8 Mon Sep 17 00:00:00 2001 From: andrea Date: Fri, 17 Jan 2025 12:01:48 +0100 Subject: [PATCH 6/8] added maintainer email address --- extra/modules/WURFL-devicedetection/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/extra/modules/WURFL-devicedetection/README.md b/extra/modules/WURFL-devicedetection/README.md index c50ee88595f..545cdf04ced 100644 --- a/extra/modules/WURFL-devicedetection/README.md +++ b/extra/modules/WURFL-devicedetection/README.md @@ -249,3 +249,7 @@ When `ext_caps` is set to `true`, the response will include all licensed capabil } } ``` + +## Maintainer + +prebid@scientiamobile.com From 3aff0372b5858974fbdcfaf64fa1d1f12bcdef81 Mon Sep 17 00:00:00 2001 From: andrea Date: Fri, 17 Jan 2025 12:35:25 +0100 Subject: [PATCH 7/8] modified sample request data --- extra/modules/WURFL-devicedetection/sample/request_data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/WURFL-devicedetection/sample/request_data.json b/extra/modules/WURFL-devicedetection/sample/request_data.json index 5184f4ca6f3..42691bbc74d 100644 --- a/extra/modules/WURFL-devicedetection/sample/request_data.json +++ b/extra/modules/WURFL-devicedetection/sample/request_data.json @@ -46,7 +46,7 @@ "domain": "test.com", "publisher": { "domain": "test.com", - "id": "3" + "id": "1" }, "page": "https://www.test.com/" }, From eedc3e0412b95f630be2f191e8c2c18d855cca02 Mon Sep 17 00:00:00 2001 From: andrea Date: Fri, 17 Jan 2025 13:16:17 +0100 Subject: [PATCH 8/8] fixed typos --- extra/modules/WURFL-devicedetection/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/modules/WURFL-devicedetection/README.md b/extra/modules/WURFL-devicedetection/README.md index 545cdf04ced..68f0fcb59ca 100644 --- a/extra/modules/WURFL-devicedetection/README.md +++ b/extra/modules/WURFL-devicedetection/README.md @@ -142,14 +142,14 @@ java -jar target/prebid-server-bundle.jar --spring.config.additional-location=sa This sample configuration contains the module hook basic configuration. All the other module configuration options are located in the `WURFL-devicedetection.yaml` inside the module. -When the server starts, it downloads the WURFL file from the `wurfl-file-url` and loads it into the module. +When the server starts, it downloads the WURFL file from the `wurfl-snapshot-url` and loads it into the module. Sample request data for testing is available in the module's `sample` directory. Using the `auction` endpoint, you can observe WURFL-enriched device data in the response. ### Sample Response -Using the sample request data via `curl` when the module is configured with `ext_caps` set to `false` (or no value) +Using the sample request data via `curl` when the module is configured with `ext-caps` set to `false` (or no value) ```bash curl http://localhost:8080/openrtb2/auction --data @extra/modules/WURFL-devicedetection/sample/request_data.json