diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 74747d3fe15..f64a14137d4 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,3 +1,3 @@
# From https://github.com/microsoft/vscode-dev-containers/blob/master/containers/go/.devcontainer/Dockerfile
-ARG VARIANT="17-jdk-bookworm"
+ARG VARIANT="21-jdk-bookworm"
FROM mcr.microsoft.com/vscode/devcontainers/java:${VARIANT}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index d167be89720..d9a309d3661 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -5,7 +5,7 @@
"dockerfile": "Dockerfile",
"args": {
// Update the VARIANT arg to pick a version of Java
- "VARIANT": "17-jdk-bookworm",
+ "VARIANT": "21-jdk-bookworm",
}
},
"containerEnv": {
diff --git a/.editorconfig b/.editorconfig
index 23e7176794a..7b8947ec3c6 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -115,7 +115,7 @@ ij_java_for_statement_wrap = off
ij_java_generate_final_locals = false
ij_java_generate_final_parameters = false
ij_java_if_brace_force = never
-ij_java_imports_layout = *,|,javax.**,java.**,|,$*
+ij_java_imports_layout = *,|,javax.**,jakarta.**,java.**,|,$*
ij_java_indent_case_from_switch = true
ij_java_insert_inner_class_imports = false
ij_java_insert_override_annotation = true
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000000..28ce307df13
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,41 @@
+### 🔧 Type of changes
+- [ ] new bid adapter
+- [ ] update bid adapter
+- [ ] new feature
+- [ ] new analytics adapter
+- [ ] new module
+- [ ] bugfix
+- [ ] documentation
+- [ ] configuration
+- [ ] tech debt (test coverage, refactorings, etc.)
+
+### ✨ What's the context?
+
+What's the context for the changes? Are there any
+
+
+### 🧠 Rationale behind the change
+
+Why did you choose to make these changes? Were there any trade-offs you had to consider?
+
+
+### 🔎 New Bid Adapter Checklist
+- [ ] verify email contact works
+- [ ] NO fully dynamic hosts
+- [ ] geographic host parameters are NOT required
+- [ ] NO direct use of HTTP is prohibited - *implement an existing Bidder interface that will do all the job*
+- [ ] if the ORTB is just forwarded to the endpoint, use the generic adapter - *define the new adapter as the alias of the generic adapter*
+- [ ] cover an adapter configuration with an integration test
+
+
+### 🧪 Test plan
+
+How do you know the changes are safe to ship to production?
+
+
+### 🏎 Quality check
+
+- [ ] Are your changes following [our code style guidelines](https://github.com/prebid/prebid-server-java/blob/master/docs/developers/code-style.md)?
+- [ ] Are there any breaking changes in your code?
+- [ ] Does your test coverage exceed 90%?
+- [ ] Are there any erroneous console logs, debuggers or leftover code in your changes?
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 00000000000..a86f0b144a5
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,48 @@
+name: "CodeQL"
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language: [ 'java' ]
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ distribution: 'temurin'
+ java-version: 21
+
+ - name: Cache Maven packages
+ uses: actions/cache@v3
+ with:
+ path: ~/.m2/repository
+ key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+ restore-keys: |
+ ${{ runner.os }}-maven-
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ with:
+ languages: ${{ matrix.language }}
+
+ - name: Build with Maven
+ run: mvn -B package --file extra/pom.xml
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
+ with:
+ category: "/language:${{ matrix.language }}"
diff --git a/.github/workflows/docker-image-publish.yml b/.github/workflows/docker-image-publish.yml
index 63d1961388d..39964eb69aa 100644
--- a/.github/workflows/docker-image-publish.yml
+++ b/.github/workflows/docker-image-publish.yml
@@ -1,10 +1,9 @@
name: Publish Docker image for new tag/release
on:
- workflow_run:
- workflows: [Publish release]
- types:
- - completed
+ push:
+ tags:
+ - '*'
env:
REGISTRY: ghcr.io
@@ -19,42 +18,55 @@ jobs:
packages: write
strategy:
matrix:
- java: [ 17 ]
- dockerfile-path: [Dockerfile, extra/Dockerfile]
+ java: [ 21 ]
+ dockerfile-path: [Dockerfile, Dockerfile-modules]
include:
- dockerfile-path: Dockerfile
build-cmd: mvn clean package -Dcheckstyle.skip -Dmaven.test.skip=true
package-name: ghcr.io/${{ github.repository }}
- - dockerfile-path: extra/Dockerfile
+
+ - dockerfile-path: Dockerfile-modules
build-cmd: mvn clean package --file extra/pom.xml -Dcheckstyle.skip -Dmaven.test.skip=true
package-name: ghcr.io/${{ github.repository }}-bundle
steps:
+ - name: Check out Repository
+ uses: actions/checkout@v4
+
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: 'temurin'
cache: 'maven'
java-version: ${{ matrix.java }}
+
- name: Build .jar via Maven
run: ${{ matrix.build-cmd }}
- - name: Checkout repository
- uses: actions/checkout@v4
+
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
- name: Extract metadata (tags, labels) for Docker Image
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ matrix.package-name }}
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ${{ matrix.dockerfile-path }}
push: true
+ platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
diff --git a/.github/workflows/issue_prioritization.yml b/.github/workflows/issue_prioritization.yml
index 784fe02656b..fa56f9ee2ee 100644
--- a/.github/workflows/issue_prioritization.yml
+++ b/.github/workflows/issue_prioritization.yml
@@ -10,7 +10,7 @@ jobs:
steps:
- name: Generate token
id: generate_token
- uses: tibdex/github-app-token@36464acb844fc53b9b8b2401da68844f6b05ebb0
+ uses: tibdex/github-app-token@v2.1.0
with:
app_id: ${{ secrets.PBS_PROJECT_APP_ID }}
private_key: ${{ secrets.PBS_PROJECT_APP_PEM }}
diff --git a/.github/workflows/pr-functional-tests.yml b/.github/workflows/pr-functional-tests.yml
index 610c6693193..e3ac3ffcd10 100644
--- a/.github/workflows/pr-functional-tests.yml
+++ b/.github/workflows/pr-functional-tests.yml
@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
- java: [ 17 ]
+ java: [ 21 ]
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/pr-java-ci.yml b/.github/workflows/pr-java-ci.yml
index 79a904c3636..3ead6423d9f 100644
--- a/.github/workflows/pr-java-ci.yml
+++ b/.github/workflows/pr-java-ci.yml
@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
- java: [ 17 ]
+ java: [ 21 ]
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/pr-module-functional-tests.yml b/.github/workflows/pr-module-functional-tests.yml
index d8f1e925a07..d87fbe4857a 100644
--- a/.github/workflows/pr-module-functional-tests.yml
+++ b/.github/workflows/pr-module-functional-tests.yml
@@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
- java: [ 17 ]
+ java: [ 21 ]
steps:
- uses: actions/checkout@v4
diff --git a/.github/workflows/release-asset-publish.yml b/.github/workflows/release-asset-publish.yml
index 1de13751c3a..fb1057d8ee8 100644
--- a/.github/workflows/release-asset-publish.yml
+++ b/.github/workflows/release-asset-publish.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- java: [ 17 ]
+ java: [ 21 ]
steps:
- uses: actions/checkout@v4
- name: Set up JDK
diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml
index b34d4827eae..c1ee08ab668 100644
--- a/.github/workflows/release-drafter.yml
+++ b/.github/workflows/release-drafter.yml
@@ -2,27 +2,20 @@ name: Publish release
on:
push:
- branches:
- - master
+ tags:
+ - '*'
jobs:
update_release_draft:
name: Publish release with notes
runs-on: ubuntu-latest
- if: "contains(github.event.head_commit.message, 'Prebid Server prepare release ')"
steps:
- - name: Extract tag from commit message
- run: |
- target_tag=${COMMIT_MSG#"Prebid Server prepare release "}
- echo "TARGET_TAG=$target_tag" >> $GITHUB_ENV
- env:
- COMMIT_MSG: ${{ github.event.head_commit.message }}
- name: Create and publish release
uses: release-drafter/release-drafter@v5
with:
config-name: release-drafter-config.yml
publish: true
- name: "v${{ env.TARGET_TAG }}"
- tag: ${{ env.TARGET_TAG }}
+ name: "v${{ github.ref_name }}"
+ tag: ${{ github.ref_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 3c057d9bda4..5f0817bd269 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,5 +13,4 @@ target/
.DS_Store
-.allure/
src/main/proto/
diff --git a/Dockerfile b/Dockerfile
index d69d5346506..b1fab501fa9 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM amazoncorretto:17
+FROM amazoncorretto:21
WORKDIR /app/prebid-server
diff --git a/Dockerfile-modules b/Dockerfile-modules
index a9cbfe71b31..d3338d2c376 100644
--- a/Dockerfile-modules
+++ b/Dockerfile-modules
@@ -1,4 +1,4 @@
-FROM amazoncorretto:17
+FROM amazoncorretto:21
WORKDIR /app/prebid-server
diff --git a/README.md b/README.md
index 44d1ffbd92b..9fbfe912715 100644
--- a/README.md
+++ b/README.md
@@ -73,8 +73,8 @@ For more information how to build the server follow [documentation](docs/build.m
## Configuration
-The source code includes an example configuration file `sample/prebid-config.yaml`.
-Also, check the account settings file `sample/sample-app-settings.yaml`.
+The source code includes an example configuration file `sample/configs/prebid-config.yaml`.
+Also, check the account settings file `sample/configs/sample-app-settings.yaml`.
For more information how to configure the server follow [documentation](docs/config.md). There are many settings you'll want to consider such as which bidders you're going to enable, privacy defaults, admin endpoints, etc.
@@ -83,7 +83,7 @@ For more information how to configure the server follow [documentation](docs/con
Run your local server with the command:
```bash
-java -jar target/prebid-server.jar --spring.config.additional-location=sample/prebid-config.yaml
+java -jar target/prebid-server.jar --spring.config.additional-location=sample/configs/prebid-config.yaml
```
For more options how to start the server, please follow [documentation](docs/run.md).
@@ -100,12 +100,30 @@ There are a couple of 'hello world' test requests described in sample/requests/R
## Running Docker image
-Starting from PBS Java v2.9, you can download prebuilt Docker images from [GitHub Packages](https://github.com/orgs/prebid/packages?repo_name=prebid-server-java) page,
-and use them instead of plain .jar files. This prebuilt images are delivered with or without extra modules.
+Starting from PBS Java v3.11.0, you can download prebuilt Docker images from [GitHub Packages](https://github.com/orgs/prebid/packages?repo_name=prebid-server-java) page,
+and use them instead of plain .jar files. These prebuilt images are delivered in 2 flavors:
+- https://github.com/prebid/prebid-server-java/pkgs/container/prebid-server-java is a bare PBS and doesn't contain modules.
+- https://github.com/prebid/prebid-server-java/pkgs/container/prebid-server-java-bundle is a "bundle" that contains PBS and all the modules.
-In order to run such image correctly, you should attach PBS config file. Easiest way is to mount config file into container,
+To run PBS from image correctly, you should provide the PBS config file. The easiest way is to mount the config file into the container,
using [--mount or --volume (-v) Docker CLI arguments](https://docs.docker.com/engine/reference/commandline/run/).
-Keep in mind, that config file should be mounted into specific location: ```/app/prebid-server/``` or ```/app/prebid-server/conf/```.
+Keep in mind that the config file should be mounted into a specific location: ```/app/prebid-server/conf/``` or ```/app/prebid-server/```.
+
+PBS follows the regular Spring Boot config load hierarchy and type.
+For simple configuration, a single `application.yaml` mounted to `/app/prebid-server/conf/` will be enough.
+Please consult [Spring Externalized Configuration](https://docs.spring.io/spring-boot/reference/features/external-config.html) for all possible ways to configure PBS.
+
+You can also supply command-line parameters through `JAVA_OPTS` environment variable which will be appended to the `java` command before the `-jar ...` parameter.
+Please pay attention to line breaks and escape them if needed.
+
+Example execution using sample configuration:
+```shell
+docker run --rm -v ./sample:/app/prebid-server/sample:ro -p 8060:8060 -p 8080:8080 ghcr.io/prebid/prebid-server-java:latest --spring.config.additional-location=sample/configs/prebid-config.yaml
+```
+or
+```shell
+docker run --rm -v ./sample:/app/prebid-server/sample:ro -p 8060:8060 -p 8080:8080 -e JAVA_OPTS=-Dspring.config.additional-location=sample/configs/prebid-config.yaml ghcr.io/prebid/prebid-server-java:latest
+```
# Documentation
diff --git a/checkstyle.xml b/checkstyle.xml
index aac9ec01cfe..aa8274c29a7 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -68,6 +68,7 @@
autovalue.shaded.com.google,
org.inferred.freebuilder.shaded.com.google,
org.apache.commons.lang"/>
+
@@ -75,7 +76,7 @@
-
+
diff --git a/docs/admin-endpoints.md b/docs/admin-endpoints.md
new file mode 100644
index 00000000000..b3176a4379c
--- /dev/null
+++ b/docs/admin-endpoints.md
@@ -0,0 +1,209 @@
+# Admin enpoints
+
+Prebid Server Java offers a set of admin endpoints for managing and monitoring the server's health, configurations, and
+metrics. Below is a detailed description of each endpoint, including HTTP methods, paths, parameters, and responses.
+
+## General settings
+
+Each endpoint can be either enabled or disabled by changing `admin-endpoints..enabled` toggle. Defaults to
+`false`.
+
+Each endpoint can be configured to serve either on application port (configured via `server.http.port` setting) or
+admin port (configured via `admin.port` setting) by changing `admin-endpoints..on-application-port`
+setting.
+By default, all admin endpoints reside on admin port.
+
+Each endpoint can be configured to serve on a certain path by setting `admin-endpoints..path`.
+
+Each endpoint can be configured to either require basic authorization or not by changing
+`admin-endpoints..protected` setting,
+defaults to `true`. Allowed credentials are globally configured for all admin endpoints with
+`admin-endpoints.credentials.`
+setting.
+
+## Endpoints
+
+1. Version info
+
+- Name: version
+- Endpoint: Configured via `admin-endpoints.version.path` setting
+- Methods:
+ - `GET`:
+ - Description: Returns the version information for the Prebid Server Java instance.
+ - Parameters: None
+ - Responses:
+ - 200 OK: JSON containing version details
+ ```json
+ {
+ "version": "x.x.x",
+ "revision": "commit-hash"
+ }
+ ```
+
+2. Currency rates
+
+- Name: currency-rates
+- Methods:
+ - `GET`:
+ - Description: Returns the latest information about currency rates used by server instance.
+ - Parameters: None
+ - Responses:
+ - 200 OK: JSON containing version details
+ ```json
+ {
+ "active": "true",
+ "source": "http://currency-source"
+ "fetchingIntervalNs": 200,
+ "lastUpdated": "02/01/2018 - 13:45:30 UTC"
+ ... Rates ...
+ }
+ ```
+
+3. Cache notification endpoint
+
+- Name: storedrequest
+- Methods:
+ - `POST`:
+ - Description: Updates stored requests/imps data stored in server instance cache.
+ - Parameters:
+ - body:
+ ```json
+ {
+ "requests": {
+ "": "",
+ ... Requests data ...
+ },
+ "imps": {
+ "": "",
+ ... Imps data ...
+ }
+ }
+ ```
+ - Responses:
+ - 200 OK
+ - 400 BAD REQUEST
+ - 405 METHOD NOT ALLOWED
+ - `DELETE`:
+ - Description: Invalidates stored requests/imps data stored in server instance cache.
+ - Parameters:
+ - body:
+ ```json
+ {
+ "requests": ["", ... Request names ...],
+ "imps": ["", ... Imp names ...]
+ }
+ ```
+ - Responses:
+ - 200 OK
+ - 400 BAD REQUEST
+ - 405 METHOD NOT ALLOWED
+
+4. Amp cache notification endpoint
+
+- Name: storedrequest-amp
+- Methods:
+ - `POST`:
+ - Description: Updates stored requests/imps data for amp, stored in server instance cache.
+ - Parameters:
+ - body:
+ ```json
+ {
+ "requests": {
+ "": "",
+ ... Requests data ...
+ },
+ "imps": {
+ "": "",
+ ... Imps data ...
+ }
+ }
+ ```
+ - Responses:
+ - 200 OK
+ - 400 BAD REQUEST
+ - 405 METHOD NOT ALLOWED
+ - `DELETE`:
+ - Description: Invalidates stored requests/imps data for amp, stored in server instance cache.
+ - Parameters:
+ - body:
+ ```json
+ {
+ "requests": ["", ... Request names ...],
+ "imps": ["", ... Imp names ...]
+ }
+ ```
+ - Responses:
+ - 200 OK
+ - 400 BAD REQUEST
+ - 405 METHOD NOT ALLOWED
+
+5. Account cache notification endpoint
+
+- Name: cache-invalidation
+- Methods:
+ - any:
+ - Description: Invalidates cached data for a provided account in server instance cache.
+ - Parameters:
+ - `account`: Account id.
+ - Responses:
+ - 200 OK
+ - 400 BAD REQUEST
+
+
+6. Http interaction logging endpoint
+
+- Name: logging-httpinteraction
+- Methods:
+ - any:
+ - Description: Changes request logging specification in server instance.
+ - Parameters:
+ - `endpoint`: Endpoint. Should be either: `auction` or `amp`.
+ - `statusCode`: Status code for logging spec.
+ - `account`: Account id.
+ - `bidder`: Bidder code.
+ - `limit`: Limit of requests for specification to be valid.
+ - Responses:
+ - 200 OK
+ - 400 BAD REQUEST
+- Additional settings:
+ - `logging.http-interaction.max-limit` - max limit for logging specification limit.
+
+7. Logging level control endpoint
+
+- Name: logging-changelevel
+- Methods:
+ - any:
+ - Description: Changes request logging level for specified amount of time in server instance.
+ - Parameters:
+ - `level`: Logging level. Should be one of: `all`, `trace`, `debug`, `info`, `warn`, `error`, `off`.
+ - `duration`: Duration of logging level (in millis) before reset to original one.
+ - Responses:
+ - 200 OK
+ - 400 BAD REQUEST
+- Additional settings:
+ - `logging.change-level.max-duration-ms` - max duration of changed logger level.
+
+8. Tracer log endpoint
+
+- Name: tracelog
+- Methods:
+ - any:
+ - Description: Adds trace logging specification for specified amount of time in server instance.
+ - Parameters:
+ - `account`: Account id.
+ - `bidderCode`: Bidder code.
+ - `level`: Log level. Should be one of: `info`, `warn`, `trace`, `error`, `fatal`, `debug`.
+ - `duration`: Duration of logging specification (in seconds).
+ - Responses:
+ - 200 OK
+ - 400 BAD REQUEST
+
+9. Collected metrics endpoint
+
+- Name: collected-metrics
+- Methods:
+ - any:
+ - Description: Adds trace logging specification for specified amount of time in server instance.
+ - Parameters: None
+ - Responses:
+ - 200 OK: JSON containing metrics data.
diff --git a/docs/application-settings.md b/docs/application-settings.md
index 4999cd293a5..bf0dc61bfd0 100644
--- a/docs/application-settings.md
+++ b/docs/application-settings.md
@@ -11,6 +11,7 @@ There are two ways to configure application settings: database and file. This do
- `auction.video-cache-ttl`- how long (in seconds) video creative will be available via the external Cache Service.
- `auction.truncate-target-attr` - Maximum targeting attributes size. Values between 1 and 255.
- `auction.default-integration` - Default integration to assume.
+- `auction.debug-allow` - enables debug output in the auction response. Default `true`.
- `auction.bid-validations.banner-creative-max-size` - Overrides creative max size validation for banners. Valid values
are:
- "skip": don't do anything about creative max size for this publisher
@@ -19,7 +20,18 @@ There are two ways to configure application settings: database and file. This do
- "enforce": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, reject
the bid and log an operational warning.
- `auction.events.enabled` - enables events for account if true
-- `auction.debug-allow` - enables debug output in the auction response. Default `true`.
+- `auction.price-floors.enabeled` - enables price floors for account if true. Defaults to true.
+- `auction.price-floors.fetch.enabled`- enables data fetch for price floors for account if true. Defaults to false.
+- `auction.price-floors.fetch.url` - url to fetch price floors data from.
+- `auction.price-floors.fetch.timeout-ms` - timeout for fetching price floors data. Defaults to 5000.
+- `auction.price-floors.fetch.max-file-size-kb` - maximum size of price floors data to be fetched. Defaults to 200.
+- `auction.price-floors.fetch.max-rules` - maximum number of rules per model group. Defaults to 0.
+- `auction.price-floors.fetch.max-age-sec` - maximum time that fetched price floors data remains in cache. Defaults to 86400.
+- `auction.price-floors.fetch.period-sec` - time between two consecutive fetches. Defaults to 3600.
+- `auction.price-floors.enforce-floors-rate` - what percentage of the time a defined floor is enforced. Default is 100.
+- `auction.price-floors.adjust-for-bid-adjustment` - boolean for whether to use the bidAdjustment function to adjust the floor per bidder. Defaults to true.
+- `auction.price-floors.enforce-deal-floors` - boolean for whether to enforce floors on deals. Defaults to true.
+- `auction.price-floors.use-dynamic-data` - boolean that can be used as an emergency override to start ignoring dynamic floors data if something goes wrong. Defaults to true.
- `auction.targeting.includewinners` - whether to include targeting for the winning bids in response. Default `false`.
- `auction.targeting.includebidderkeys` - whether to include targeting for the best bid from each bidder in response. Default `false`.
- `auction.targeting.includeformat` - whether to include the “hb_format” targeting key. Default `false`.
@@ -30,39 +42,61 @@ Keep in mind following restrictions:
- this prefix value may be overridden by correspond property from bid request
- prefix length is limited by `auction.truncate-target-attr`
- if custom prefix may produce keywords that exceed `auction.truncate-target-attr`, prefix value will drop to default `hb`
-- `privacy.ccpa.enabled` - enables gdpr verifications if true. Has higher priority than configuration in application.yaml.
-- `privacy.ccpa.channel-enabled.web` - overrides `ccpa.enforce` property behaviour for web requests type.
-- `privacy.ccpa.channel-enabled.amp` - overrides `ccpa.enforce` property behaviour for amp requests type.
-- `privacy.ccpa.channel-enabled.app` - overrides `ccpa.enforce` property behaviour for app requests type.
-- `privacy.ccpa.channel-enabled.video` - overrides `ccpa.enforce` property behaviour for video requests type.
+- `auction.preferredmediatype..` - that will be left for that doesn't support multi-format. Other media types will be removed. Acceptable values: `banner`, `video`, `audio`, `native`.
+- `auction.privacysandbox.cookiedeprecation.enabled` - boolean that turns on setting and reading of the Chrome Privacy Sandbox testing label header. Defaults to false.
+- `auction.privacysandbox.cookiedeprecation.ttlsec` - if the above setting is true, how long to set the receive-cookie-deprecation cookie's expiration
- `privacy.gdpr.enabled` - enables gdpr verifications if true. Has higher priority than configuration in
application.yaml.
+- `privacy.gdpr.eea-countries` - overrides the host-level list of 2-letter country codes where TCF processing is applied
- `privacy.gdpr.channel-enabled.web` - overrides `privacy.gdpr.enabled` property behaviour for web requests type.
- `privacy.gdpr.channel-enabled.amp` - overrides `privacy.gdpr.enabled` property behaviour for amp requests type.
- `privacy.gdpr.channel-enabled.app` - overrides `privacy.gdpr.enabled` property behaviour for app requests type.
- `privacy.gdpr.channel-enabled.video` - overrides `privacy.gdpr.enabled` property behaviour for video requests
type.
+- `privacy.gdpr.channel-enabled.dooh` - overrides `privacy.gdpr.enabled` property behaviour for dooh requests
+ type.
- `privacy.gdpr.purposes.[p1-p10].enforce-purpose` - define type of enforcement confirmation: `no`/`basic`/`full`.
Default `full`
- `privacy.gdpr.purposes.[p1-p10].enforce-vendors` - if equals to `true`, user must give consent to use vendors.
Purposes will be omitted. Default `true`
- `privacy.gdpr.purposes.[p1-p10].vendor-exceptions[]` - bidder names that will be treated opposite
to `pN.enforce-vendors` value.
-- `privacy.gdpr.special-features.[f1-f2].enforce`- if equals to `true`, special feature will be enforced for purpose.
+- `privacy.gdpr.purposes.p4.eid.activity_transition` - defaults to `true`. If `true` and transmitEids is not specified, but transmitUfpd is specified, then the logic of transmitUfpd is used. This is to avoid breaking changes to existing configurations. The default value of the flag will be changed in a future release.
+- `privacy.gdpr.purposes.p4.eid.require_consent` - if equals to `true`, transmitting EIDs require P4 legal basis unless excepted.
+- `privacy.gdpr.purposes.p4.eid.exceptions` - list of EID sources that are excepted from P4 enforcement and will be transmitted if any P2-P10 is consented.
+- `privacy.gdpr.special-features.[sf1-sf2].enforce`- if equals to `true`, special feature will be enforced for purpose.
Default `true`
-- `privacy.gdpr.special-features.[f1-f2].vendor-exceptions` - bidder names that will be treated opposite
+- `privacy.gdpr.special-features.[sf1-sf2].vendor-exceptions` - bidder names that will be treated opposite
to `sfN.enforce` value.
- `privacy.gdpr.purpose-one-treatment-interpretation` - option that allows to skip the Purpose one enforcement workflow.
Values: ignore, no-access-allowed, access-allowed.
-- `metrics.verbosity-level` - defines verbosity level of metrics for this account, overrides `metrics.accounts` application settings configuration.
+- `privacy.gdpr.basic-enforcement-vendors` - bypass vendor-level checks for these biddercodes.
+- `privacy.ccpa.enabled` - enables gdpr verifications if true. Has higher priority than configuration in application.yaml.
+- `privacy.ccpa.channel-enabled.web` - overrides `ccpa.enforce` property behaviour for web requests type.
+- `privacy.ccpa.channel-enabled.amp` - overrides `ccpa.enforce` property behaviour for amp requests type.
+- `privacy.ccpa.channel-enabled.app` - overrides `ccpa.enforce` property behaviour for app requests type.
+- `privacy.ccpa.channel-enabled.video` - overrides `ccpa.enforce` property behaviour for video requests type.
+- `privacy.ccpa.channel-enabled.dooh` - overrides `ccpa.enforce` property behaviour for dooh requests type.
+- `privacy.dsa.default.dsarequired` - inject this dsarequired value for this account. See https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md for details.
+- `privacy.dsa.default.pubrender` - inject this pubrender value for this account. See https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md for details.
+- `privacy.dsa.default.datatopub` - inject this datatopub value for this account. See https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md for details.
+- `privacy.dsa.default.transparency[].domain` - inject this domain value for this account. See https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md for details.
+- `privacy.dsa.default.transparency[].dsaparams` - inject this dsaparams value for this account. See https://github.com/InteractiveAdvertisingBureau/openrtb/blob/main/extensions/community_extensions/dsa_transparency.md for details.
+- `privacy.dsa.gdpr-only` - When true, DSA default injection only happens when in GDPR scope. Defaults to false, meaning all the time.
+- `privacy.allowactivities` - configuration for Activity Infrastructure. For further details, see: https://docs.prebid.org/prebid-server/features/pbs-activitycontrols.html
+- `privacy.modules` - configuration for Privacy Modules. Each privacy module have own configuration.
+- `analytics.allow-client-details` - when true, this boolean setting allows responses to transmit the server-side analytics tags to support client-side analytics adapters. Defaults to false.
- `analytics.auction-events.` - defines which channels are supported by analytics for this account
- `analytics.modules..*` - space for `module-name` analytics module specific configuration, may be of any shape
-- `cookie-sync.default-timeout-ms` - overrides host level config
+- `analytics.modules..*` - a space for specific data for the analytics adapter, which may include an enabled property to control whether the adapter should be triggered, along with other adapter-specific properties. These will be merged under `ext.prebid.analytics.` in the request.
+- `metrics.verbosity-level` - defines verbosity level of metrics for this account, overrides `metrics.accounts` application settings configuration.
- `cookie-sync.default-limit` - if the "limit" isn't specified in the `/cookie_sync` request, this is what to use
-- `cookie-sync.pri` - a list of prioritized bidder codes
- `cookie-sync.max-limit` - if the "limit" is specified in the `/cookie_sync` request, it can't be greater than this
value
+- `cookie-sync.pri` - a list of prioritized bidder codes
- `cookie-sync.coop-sync.default` - if the "coopSync" value isn't specified in the `/cookie_sync` request, use this
+- `hooks` - configuration for Prebid Server Modules. For further details, see: https://docs.prebid.org/prebid-server/pbs-modules/index.html#2-define-an-execution-plan
+- `settings.geo-lookup` - enables geo lookup for account if true. Defaults to false.
Here are the definitions of the "purposes" that can be defined in the GDPR setting configurations:
@@ -226,6 +260,51 @@ Here's an example YAML file containing account-specific settings:
default: true
```
+## Setting Account Configuration in S3
+
+This is identical to the account configuration in a file system, with the main difference that your file system is
+[AWS S3](https://aws.amazon.com/de/s3/) or any S3 compatible storage, such as [MinIO](https://min.io/).
+
+
+The general idea is that you'll place all the account-specific settings in a separate YAML file and point to that file.
+
+```yaml
+settings:
+ s3:
+ accessKeyId:
+ secretAccessKey:
+ endpoint: # http://s3.storage.com
+ bucket: # prebid-application-settings
+ region: # if not provided AWS_GLOBAL will be used. Example value: 'eu-central-1'
+ accounts-dir: accounts
+ stored-imps-dir: stored-impressions
+ stored-requests-dir: stored-requests
+ stored-responses-dir: stored-responses
+
+ # recommended to configure an in memory cache, but this is optional
+ in-memory-cache:
+ # example settings, tailor to your needs
+ cache-size: 100000
+ ttl-seconds: 1200 # 20 minutes
+ # recommended to configure
+ s3-update:
+ refresh-rate: 900000 # Refresh every 15 minutes
+ timeout: 5000
+```
+
+### File format
+
+We recommend using the `json` format for your account configuration. A minimal configuration may look like this.
+
+```json
+{
+ "id" : "979c7116-1f5a-43d4-9a87-5da3ccc4f52c",
+ "status" : "active"
+}
+```
+
+This pairs nicely if you have a default configuration defined in your prebid server config under `settings.default-account-config`.
+
## Setting Account Configuration in the Database
In database approach account properties are stored in database table(s).
diff --git a/docs/build-aws.md b/docs/build-aws.md
index c6e64d93630..2dd19ed6811 100644
--- a/docs/build-aws.md
+++ b/docs/build-aws.md
@@ -1,3 +1,6 @@
+## Deploying through _Prebid Server Deployment on AWS_ Solution
+Prebid Server can be automatically deployed into an AWS account using the [Prebid Server Deployment on AWS](https://aws.amazon.com/solutions/implementations/prebid-server-deployment-on-aws/) Solution. Users retain full control over bidding decision logic and transaction data for real-time ad monetization, within their own AWS environment. It also offers enterprise-grade scalability to handle a variety of requests and enhances data protection using the robust security capabilities of the AWS Cloud. It is [open-source](https://github.com/aws-solutions/prebid-server-deployment-on-aws) and includes a comprehensive [Implementation Guide](https://docs.aws.amazon.com/pdfs/solutions/latest/prebid-server-deployment-on-aws/prebid-server-deployment-on-aws.pdf) and the accompanying [AWS CloudFormation template](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?templateURL=https://solutions-reference.s3.amazonaws.com/prebid-server-deployment-on-aws/latest/prebid-server-deployment-on-aws.template&redirectId=SolutionWeb) for a one-click launch.
+
## Creating project ZIP package and deploying it to AWS Elastic Beanstalk
Follow next steps to create zip which can be deployed to AWS Elastic Beanstalk.
@@ -44,7 +47,7 @@ where
If you follow same naming convention, your `run.sh` script should be similar to:
```
-exec java -jar prebid-server.jar -Dlogging.config=prebid-logging.xml --spring.config.additional-location=sample/prebid-config.yaml
+exec java -jar prebid-server.jar -Dlogging.config=prebid-logging.xml --spring.config.additional-location=sample/configs/prebid-config.yaml
```
Make run.sh executable using the next command:
diff --git a/docs/config-app.md b/docs/config-app.md
index 79cbf9f85ef..40a2c42784e 100644
--- a/docs/config-app.md
+++ b/docs/config-app.md
@@ -22,10 +22,10 @@ This parameter exists to allow to change the location of the directory Vert.x wi
- `server.jks-password` - password for the keystore (if ssl is enabled).
## HTTP Server
-- `http.max-headers-size` - set the maximum length of all headers, deprecated(use server.max-headers-size instead).
-- `http.ssl` - enable SSL/TLS support, deprecated(use server.ssl instead).
-- `http.jks-path` - path to the java keystore (if ssl is enabled), deprecated(use server.jks-path instead).
-- `http.jks-password` - password for the keystore (if ssl is enabled), deprecated(use server.jks-password instead).
+- `server.max-headers-size` - set the maximum length of all headers, deprecated(use server.max-headers-size instead).
+- `server.ssl` - enable SSL/TLS support, deprecated(use server.ssl instead).
+- `server.jks-path` - path to the java keystore (if ssl is enabled), deprecated(use server.jks-path instead).
+- `server.jks-password` - password for the keystore (if ssl is enabled), deprecated(use server.jks-password instead).
- `server.http.server-instances` - how many http server instances should be created.
This parameter affects how many CPU cores will be utilized by the application. Rough assumption - one http server instance will keep 1 CPU core busy.
- `server.http.enabled` - if set to `true` enables http server
@@ -61,6 +61,10 @@ Removes and downloads file again if depending service cant process probably corr
- `.remote-file-syncer.tmp-filepath` - full path to the temporary file.
- `.remote-file-syncer.retry-count` - how many times try to download.
- `.remote-file-syncer.retry-interval-ms` - how long to wait between failed retries.
+- `.remote-file-syncer.retry.delay-millis` - initial time of how long to wait between failed retries.
+- `.remote-file-syncer.retry.max-delay-millis` - maximum allowed value for `delay-millis`.
+- `.remote-file-syncer.retry.factor` - factor for the `delay-millis` value, that will be applied after each failed retry to modify `delay-millis` value.
+- `.remote-file-syncer.retry.jitter` - jitter (multiplicative) for `delay-millis` parameter.
- `.remote-file-syncer.timeout-ms` - default operation timeout for obtaining database file.
- `.remote-file-syncer.update-interval-ms` - time interval between updates of the usable file.
- `.remote-file-syncer.http-client.connect-timeout-ms` - set the connect timeout.
@@ -75,9 +79,8 @@ Removes and downloads file again if depending service cant process probably corr
- `default-request.file.path` - path to a JSON file containing the default request
## Auction (OpenRTB)
-- `auction.blacklisted-accounts` - comma separated list of blacklisted account IDs.
-- `auction.blacklisted-apps` - comma separated list of blacklisted applications IDs, requests from which should not be processed.
-- `auction.max-timeout-ms` - maximum operation timeout for OpenRTB Auction requests. Deprecated.
+- `auction.blocklisted-accounts` - comma separated list of blocklisted account IDs.
+- `auction.blocklisted-apps` - comma separated list of blocklisted applications IDs, requests from which should not be processed.
- `auction.biddertmax.min` - minimum operation timeout for OpenRTB Auction requests.
- `auction.biddertmax.max` - maximum operation timeout for OpenRTB Auction requests.
- `auction.biddertmax.percent` - adjustment factor for `request.tmax` for bidders.
@@ -92,6 +95,7 @@ Removes and downloads file again if depending service cant process probably corr
- `auction.validations.secure-markup` - enables secure markup validation. Possible values: `skip`, `enforce`, `warn`. Default is `skip`.
- `auction.host-schain-node` - defines global schain node that will be appended to `request.source.ext.schain.nodes` passed to bidders
- `auction.category-mapping-enabled` - if equals to `true` the category mapping feature will be active while auction.
+- `auction.strict-app-site-dooh` - if set to `true`, it will reject requests that contain more than one of app/site/dooh. Defaults to `false`.
## Event
- `event.default-timeout-ms` - timeout for event notifications
@@ -104,7 +108,7 @@ Removes and downloads file again if depending service cant process probably corr
## Video
- `auction.video.stored-required` - flag forces to merge with stored request
-- `auction.blacklisted-accounts` - comma separated list of blacklisted account IDs.
+- `auction.blocklisted-accounts` - comma separated list of blocklisted account IDs.
- `video.stored-requests-timeout-ms` - timeout for stored requests fetching.
- `auction.ad-server-currency` - default currency for video auction, if its value was not specified in request. Important note: PBS uses ISO-4217 codes for the representation of currencies.
- `auction.video.escape-log-cache-regex` - regex to remove from cache debug log xml.
@@ -202,33 +206,13 @@ Also, each bidder could have its own bidder-specific options.
- `admin-endpoints.tracelog.enabled` - if equals to `true` the endpoint will be available.
- `admin-endpoints.tracelog.path` - the server context path where the endpoint will be accessible.
- `admin-endpoints.tracelog.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`.
-- `admin-endpoints.tracelog.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials`
-
-- `admin-endpoints.deals-status.enabled` - if equals to `true` the endpoint will be available.
-- `admin-endpoints.deals-status.path` - the server context path where the endpoint will be accessible.
-- `admin-endpoints.deals-status.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`.
-- `admin-endpoints.deals-status.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials`
-
-- `admin-endpoints.lineitem-status.enabled` - if equals to `true` the endpoint will be available.
-- `admin-endpoints.lineitem-status.path` - the server context path where the endpoint will be accessible.
-- `admin-endpoints.lineitem-status.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`.
-- `admin-endpoints.lineitem-status.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials`
-
-- `admin-endpoints.e2eadmin.enabled` - if equals to `true` the endpoint will be available.
-- `admin-endpoints.e2eadmin.path` - the server context path where the endpoint will be accessible.
-- `admin-endpoints.e2eadmin.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`.
-- `admin-endpoints.e2eadmin.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials`
+- `admin-endpoints.tracelog.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials`
- `admin-endpoints.collected-metrics.enabled` - if equals to `true` the endpoint will be available.
- `admin-endpoints.collected-metrics.path` - the server context path where the endpoint will be accessible.
- `admin-endpoints.collected-metrics.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`.
- `admin-endpoints.collected-metrics.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials`
-- `admin-endpoints.force-deals-update.enabled` - if equals to `true` the endpoint will be available.
-- `admin-endpoints.force-deals-update.path` - the server context path where the endpoint will be accessible.
-- `admin-endpoints.force-deals-update.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`.
-- `admin-endpoints.force-deals-update.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials`
-
- `admin-endpoints.credentials` - user and password for access to admin endpoints if `admin-endpoints.[NAME].protected` is true`.
## Metrics
@@ -237,6 +221,12 @@ Also, each bidder could have its own bidder-specific options.
So far metrics cannot be submitted simultaneously to many backends. Currently we support `graphite` and `influxdb`.
Also, for debug purposes you can use `console` as metrics backend.
+For `logback` backend type available next options:
+- `metrics.logback.enabled` - if equals to `true` then logback reporter will be started.
+- `metrics.logback.name` - name of logger element in the logback configuration file.
+- `metrics.logback.interval` - interval in seconds between successive sending metrics.
+
+
For `graphite` backend type available next options:
- `metrics.graphite.enabled` - if equals to `true` then `graphite` will be used to submit metrics.
- `metrics.graphite.prefix` - the prefix of all metric names.
@@ -278,6 +268,9 @@ See [metrics documentation](metrics.md) for complete list of metrics submitted a
- `cache.scheme` - set the external Cache Service protocol: `http`, `https`, etc.
- `cache.host` - set the external Cache Service destination in format `host:port`.
- `cache.path` - set the external Cache Service path, for example `/cache`.
+- `storage.pbc.enabled` - If set to true, this will allow storing modules’ data in third-party storage.
+- `storage.pbc.path` - set the external Cache Service path for module caching, for example `/pbc-storage`.
+- `pbc.api.key` - set the external Cache Service api key for secured calls.
- `cache.query` - appends to the cache path as query string params (used for legacy Auction requests).
- `cache.banner-ttl-seconds` - how long (in seconds) banner will be available via the external Cache Service.
- `cache.video-ttl-seconds` - how long (in seconds) video creative will be available via the external Cache Service.
@@ -309,8 +302,10 @@ For database data source available next options:
- `settings.database.user` - database user.
- `settings.database.password` - database password.
- `settings.database.pool-size` - set the initial/min/max pool size of database connections.
+- `settings.database.idle-connection-timeout` - Set the idle timeout, time unit is seconds. Zero means don't timeout. This determines if a connection will timeout and be closed and get back to the pool if no data is received nor sent within the timeout.
+- `settings.database.enable-prepared-statement-caching` - Enable caching of the prepared statements so that they can be reused. Defaults to `false`. Please be vary of the DB server limitations as cache instances is per-database-connection.
+- `settings.database.max-prepared-statement-cache-size` - Set the maximum size of the prepared statement cache. Defaults to `256`. Has any effect only when `settings.database.enable-prepared-statement-caching` is set to `true`. Please note that the cache size is multiplied by `settings.database.pool-size`.
- `settings.database.account-query` - the SQL query to fetch account.
-- `settings.database.provider-class` - type of connection pool to be used: `hikari` or `c3p0`.
- `settings.database.stored-requests-query` - the SQL query to fetch stored requests.
- `settings.database.amp-stored-requests-query` - the SQL query to fetch AMP stored requests.
- `settings.database.stored-responses-query` - the SQL query to fetch stored responses.
@@ -353,6 +348,7 @@ See [application settings](application-settings.md) for full reference of availa
For caching available next options:
- `settings.in-memory-cache.ttl-seconds` - how long (in seconds) data will be available in LRU cache.
- `settings.in-memory-cache.cache-size` - the size of LRU cache.
+- `settings.in-memory-cache.jitter-seconds` - jitter (in seconds) for `settings.in-memory-cache.ttl-seconds` parameter.
- `settings.in-memory-cache.notification-endpoints-enabled` - if equals to `true` two additional endpoints will be
available: [/storedrequests/openrtb2](endpoints/storedrequests/openrtb2.md) and [/storedrequests/amp](endpoints/storedrequests/amp.md).
- `settings.in-memory-cache.account-invalidation-enabled` - if equals to `true` additional admin protected endpoints will be
@@ -361,14 +357,14 @@ available: `/cache/invalidate?account={accountId}` which remove account from the
- `settings.in-memory-cache.http-update.amp-endpoint` - the url to fetch AMP stored request updates.
- `settings.in-memory-cache.http-update.refresh-rate` - refresh period in ms for stored request updates.
- `settings.in-memory-cache.http-update.timeout` - timeout for obtaining stored request updates.
-- `settings.in-memory-cache.jdbc-update.init-query` - initial query for fetching all stored requests at the startup.
-- `settings.in-memory-cache.jdbc-update.update-query` - a query for periodical update of stored requests, that should
-contain 'WHERE last_updated > ?' to fetch only the records that were updated since previous check.
-- `settings.in-memory-cache.jdbc-update.amp-init-query` - initial query for fetching all AMP stored requests at the startup.
-- `settings.in-memory-cache.jdbc-update.amp-update-query` - a query for periodical update of AMP stored requests, that should
-contain 'WHERE last_updated > ?' to fetch only the records that were updated since previous check.
-- `settings.in-memory-cache.jdbc-update.refresh-rate` - refresh period in ms for stored request updates.
-- `settings.in-memory-cache.jdbc-update.timeout` - timeout for obtaining stored request updates.
+- `settings.in-memory-cache.database-update.init-query` - initial query for fetching all stored requests at the startup.
+- `settings.in-memory-cache.database-update.update-query` - a query for periodical update of stored requests, that should
+contain 'WHERE last_updated > ?' for MySQL and 'WHERE last_updated > $1' for Postgresql to fetch only the records that were updated since previous check.
+- `settings.in-memory-cache.database-update.amp-init-query` - initial query for fetching all AMP stored requests at the startup.
+- `settings.in-memory-cache.database-update.amp-update-query` - a query for periodical update of AMP stored requests, that should
+contain 'WHERE last_updated > ?' for MySQL and 'WHERE last_updated > $1' for Postgresql to fetch only the records that were updated since previous check.
+- `settings.in-memory-cache.database-update.refresh-rate` - refresh period in ms for stored request updates.
+- `settings.in-memory-cache.database-update.timeout` - timeout for obtaining stored request updates.
For targeting available next options:
- `settings.targeting.truncate-attr-chars` - set the max length for names of targeting keywords (0 means no truncation).
@@ -433,6 +429,7 @@ If not defined in config all other Health Checkers would be disabled and endpoin
- `geolocation.maxmind.remote-file-syncer` - use RemoteFileSyncer component for downloading/updating MaxMind database file. See [RemoteFileSyncer](#remote-file-syncer) section for its configuration.
## Analytics
+- `analytics.global.adapters` - Names of analytics adapters that will work for each request, except those disabled at the account level.
- `analytics.pubstack.enabled` - if equals to `true` the Pubstack analytics module will be enabled. Default value is `false`.
- `analytics.pubstack.endpoint` - url for reporting events and fetching configuration.
- `analytics.pubstack.scopeid` - defined the scope provided by the Pubstack Support Team.
@@ -442,41 +439,6 @@ If not defined in config all other Health Checkers would be disabled and endpoin
- `analytics.pubstack.buffers.count` - threshold in events count for buffer to send events
- `analytics.pubstack.buffers.report-ttl-ms` - max period between two reports.
-## Programmatic Guaranteed Delivery
-- `deals.planner.plan-endpoint` - planner endpoint to get plans from.
-- `deals.planner.update-period` - cron expression to start job for requesting Line Item metadata updates from the Planner.
-- `deals.planner.plan-advance-period` - cron expression to start job for advancing Line Items to the next plan.
-- `deals.planner.retry-period-sec` - how long (in seconds) to wait before re-sending a request to the Planner that previously failed with 5xx HTTP error code.
-- `deals.planner.timeout-ms` - default operation timeout for requests to planner's endpoints.
-- `deals.planner.register-endpoint` - register endpoint to get plans from.
-- `deals.planner.register-period-sec` - time period (in seconds) to send register request to the Planner.
-- `deals.planner.username` - username for planner BasicAuth.
-- `deals.planner.password` - password for planner BasicAuth.
-- `deals.delivery-stats.delivery-period` - cron expression to start job for sending delivery progress to planner.
-- `deals.delivery-stats.cached-reports-number` - how many reports to cache while planner is unresponsive.
-- `deals.delivery-stats.timeout-ms` - default operation timeout for requests to delivery progress endpoints.
-- `deals.delivery-stats.username` - username for delivery progress BasicAuth.
-- `deals.delivery-stats.password` - password for delivery progress BasicAuth.
-- `deals.delivery-stats.line-items-per-report` - max number of line items in each report to split for batching. Default is 25.
-- `deals.delivery-stats.reports-interval-ms` - interval in ms between consecutive reports. Default is 0.
-- `deals.delivery-stats.batches-interval-ms` - interval in ms between consecutive batches. Default is 1000.
-- `deals.delivery-stats.request-compression-enabled` - enables request gzip compression when set to true.
-- `deals.delivery-progress.line-item-status-ttl-sec` - how long to store line item's metrics after it was expired.
-- `deals.delivery-progress.cached-plans-number` - how many plans to store in metrics per line item.
-- `deals.delivery-progress.report-reset-period`- cron expression to start job for closing current delivery progress and starting new one.
-- `deals.delivery-progress-report.competitors-number`- number of line items top competitors to send in delivery progress report.
-- `deals.user-data.user-details-endpoint` - user Data Store endpoint to get user details from.
-- `deals.user-data.win-event-endpoint` - user Data Store endpoint to which win events should be sent.
-- `deals.user-data.timeout` - time to wait (in milliseconds) for User Data Service response.
-- `deals.user-data.user-ids` - list of Rules for determining user identifiers to send to User Data Store.
-- `deals.max-deals-per-bidder` - maximum number of deals to send to each bidder.
-- `deals.alert-proxy.enabled` - enable alert proxy service if `true`.
-- `deals.alert-proxy.url` - alert service endpoint to send alerts to.
-- `deals.alert-proxy.timeout-sec` - default operation timeout for requests to alert service endpoint.
-- `deals.alert-proxy.username` - username for alert proxy BasicAuth.
-- `deals.alert-proxy.password` - password for alert proxy BasicAuth.
-- `deals.alert-proxy.alert-types` - key value pair of alert type and sampling factor to send high priority alert.
-
## Debugging
- `debug.override-token` - special string token for overriding Prebid Server account and/or adapter debug information presence in the auction response.
diff --git a/docs/deals.md b/docs/deals.md
deleted file mode 100644
index fca8c585e26..00000000000
--- a/docs/deals.md
+++ /dev/null
@@ -1,152 +0,0 @@
-# Deals
-
-## Planner and Register services
-
-### Planner service
-
-Periodically request Line Item metadata from the Planner. Line Item metadata includes:
-1. Line Item details
-2. Targeting
-3. Frequency caps
-4. Delivery schedule
-
-### Register service
-
-Each Prebid Server instance register itself with the General Planner with a health index
-(QoS indicator based on its internal counters like circuit breaker trip counters, timeouts, etc.)
-and KPI like ad requests per second.
-
-Also allows planner send command to PBS admin endpoint to stored request caches and tracelogs.
-
-### Planner and register service configuration
-
-```yaml
-planner:
- register-endpoint:
- plan-endpoint:
- update-period: "0 */1 * * * *"
- register-period-sec: 60
- timeout-ms: 8000
- username:
- password:
-```
-
-## Deals stats service
-
-Supports sending reports to delivery stats serving with following metrics:
-
-1. Number of client requests seen since start-up
-2. For each Line Item
-- Number of tokens spent so far at each token class within active and expired plans
-- Number of times the account made requests (this will be the same across all LineItem for the account)
-- Number of win notifications
-- Number of times the domain part of the target matched
-- Number of times impressions matched whole target
-- Number of times impressions matched the target but was frequency capped
-- Number of times impressions matched the target but the fcap lookup failed
-- Number of times LineItem was sent to the bidder
-- Number of times LineItem was sent to the bidder as the top match
-- Number of times LineItem came back from the bidder
-- Number of times the LineItem response was invalidated
-- Number of times the LineItem was sent to the client
-- Number of times the LineItem was sent to the client as the top match
-- Array of top 10 competing LineItems sent to client
-
-### Deals stats service configuration
-
-```yaml
-delivery-stats:
- endpoint:
- delivery-period: "0 */1 * * * *"
- cached-reports-number: 20
- line-item-status-ttl-sec: 3600
- timeout-ms: 8000
- username:
- password:
-```
-
-## Alert service
-
-Sends out alerts when PBS cannot talk to general planner and other critical situations. Alerts are simply JSON messages
-over HTTP sent to a central proxy server.
-
-```yaml
- alert-proxy:
- enabled: truew
- timeout-sec: 10
- url:
- username:
- password:
- alert-types:
- :
- pbs-planner-empty-response-error: 15
-```
-
-## GeoLocation service
-
-This service currently has 1 implementation:
-- MaxMind
-
-In order to support targeting by geographical attributes the service will provide the following information:
-
-1. `continent` - Continent code
-2. `region` - Region code using ISO-3166-2
-3. `metro` - Nielsen DMAs
-4. `city` - city using provider specific encoding
-5. `lat` - latitude from -90.0 to +90.0, where negative is south
-6. `lon` - longitude from -180.0 to +180.0, where negative is west
-
-### GeoLocation service configuration for MaxMind
-
-```yaml
-geolocation:
- enabled: true
- type: maxmind
- maxmind:
- remote-file-syncer:
- download-url:
- save-filepath:
- tmp-filepath:
- retry-count: 3
- retry-interval-ms: 3000
- timeout-ms: 300000
- update-interval-ms: 0
- http-client:
- connect-timeout-ms: 2500
- max-redirects: 3
-```
-
-## User Service
-
-This service is responsible for:
-- Requesting user targeting segments and frequency capping status from the User Data Store
-- Reporting to User Data Store when users finally see ads to aid in correctly enforcing frequency caps
-
-### User service configuration
-
-```yaml
- user-data:
- win-event-endpoint:
- user-details-endpoint:
- timeout: 1000
- user-ids:
- - location: rubicon
- source: uid
- type: khaos
-```
-1. khaos, adnxs - types of the ids that will be specified in requests to User Data Store
-2. source - source of the id, the only supported value so far is “uids” which stands for uids cookie
-3. location - where exactly in the source to look for id
-
-## Device Info Service
-
-DeviceInfoService returns device-related attributes based on User-Agent for use in targeting:
-- deviceClass: desktop, tablet, phone, ctv
-- os: windows, ios, android, osx, unix, chromeos
-- osVersion
-- browser: chrome, firefox, edge, safari
-- browserVersion
-
-## See also
-
-- [Configuration](config.md)
diff --git a/docs/developers/code-reviews.md b/docs/developers/code-reviews.md
index 78728fef18a..cc8ed667849 100644
--- a/docs/developers/code-reviews.md
+++ b/docs/developers/code-reviews.md
@@ -43,3 +43,4 @@ explaining it. Are there better ways to achieve those goals?
- Does the code use any global, mutable state? [Inject dependencies](https://en.wikipedia.org/wiki/Dependency_injection) instead!
- Can the code be organized into smaller, more modular pieces?
- Is there dead code which can be deleted? Or TODO comments which should be resolved?
+- Look for code used by other adapters. Encourage adapter submitter to utilize common code.
diff --git a/docs/developers/functional-tests.md b/docs/developers/functional-tests.md
index fd216eb89c5..523466fb0b0 100644
--- a/docs/developers/functional-tests.md
+++ b/docs/developers/functional-tests.md
@@ -70,7 +70,7 @@ Functional tests need to have name template **.\*Spec.groovy**
**Properties:**
`launchContainers` - responsible for starting the MockServer and the MySQLContainer container. Default value is false to not launch containers for unit tests.
-`tests.max-container-count` - maximum number of simultaneously running PBS containers. Default value is 2.
+`tests.max-container-count` - maximum number of simultaneously running PBS containers. Default value is 5.
`skipFunctionalTests` - allow to skip funtional tests. Default value is false.
`skipUnitTests` - allow to skip unit tests. Default value is false.
@@ -131,7 +131,16 @@ Container for mocking different calls from PBS: prebid cache, bidders, currency
Container for Mysql database.
-- Use `org/prebid/server/functional/db_schema.sql` script for scheme.
+- Use `org/prebid/server/functional/db_mysql_schema.sql` script for scheme.
+- DataBase: `prebid`
+- Username: `prebid`
+- Password: `prebid`
+
+#### PostgreSQLContainer
+
+Container for PostgreSQL database.
+
+- Use `org/prebid/server/functional/db_psql_schema.sql` script for scheme.
- DataBase: `prebid`
- Username: `prebid`
- Password: `prebid`
diff --git a/docs/metrics.md b/docs/metrics.md
index 41dd45cc916..11f2165978c 100644
--- a/docs/metrics.md
+++ b/docs/metrics.md
@@ -11,7 +11,7 @@ Other available metrics not mentioned here can found at
where:
- `[IP]` should be equal to IP address of bound network interface on cluster node for Prebid Server (for example: `0.0.0.0`)
-- `[PORT]` should be equal to `http.port` configuration property
+- `[PORT]` should be equal to `server.http.port` configuration property
### HTTP client metrics
- `vertx.http.clients.connections.{min,max,mean,p95,p99}` - how long connections live
@@ -44,7 +44,7 @@ where `[DATASOURCE]` is a data source name, `DEFAULT_DS` by defaul.
- `imps_video` - number of video impressions
- `imps_native` - number of native impressions
- `imps_audio` - number of audio impressions
-- `requests.(ok|badinput|err|networkerr|blacklisted_account|blacklisted_app).(openrtb2-web|openrtb-app|amp|legacy)` - number of requests broken down by status and type
+- `requests.(ok|badinput|err|networkerr|blocklisted_account|blocklisted_app).(openrtb2-web|openrtb-app|amp|legacy)` - number of requests broken down by status and type
- `bidder-cardinality..requests` - number of requests targeting `` of bidders
- `connection_accept_errors` - number of errors occurred while establishing HTTP connection
- `db_query_time` - timer tracking how long did it take for database client to obtain the result for a query
@@ -133,29 +133,3 @@ Following metrics are collected and submitted if account is configured with `det
- `analytics..(auction|amp|video|cookie_sync|event|setuid).timeout` - number of event requests, failed with timeout cause
- `analytics..(auction|amp|video|cookie_sync|event|setuid).err` - number of event requests, failed with errors
- `analytics..(auction|amp|video|cookie_sync|event|setuid).badinput` - number of event requests, rejected with bad input cause
-
-## win notifications
-- `win_notifications` - total number of win notifications.
-- `win_requests` - total number of requests sent to user service for win notifications.
-- `win_request_preparation_failed` - number of request failed validation and were not sent.
-- `win_request_time` - latency between request to user service and response for win notifications.
-- `win_request_failed` - number of failed request sent to user service for win notifications.
-- `win_request_successful` - number of successful request sent to user service for win notifications.
-
-## user details
-- `user_details_requests` - total number of requests sent to user service to get user details.
-- `user_details_request_preparation_failed` - number of request failed validation and were not sent.
-- `user_details_request_time` - latency between request to user service and response to get user details.
-- `user_details_request_failed` - number of failed request sent to user service to get user details.
-- `user_details_request_successful` - number of successful request sent to user service to get user details.
-
-## Programmatic guaranteed metrics
-- `pg.planner_lineitems_received` - number of line items received from general planner.
-- `pg.planner_requests` - total number of requests sent to general planner.
-- `pg.planner_request_failed` - number of failed request sent to general planner.
-- `pg.planner_request_successful` - number of successful requests sent to general planner.
-- `pg.planner_request_time` - latency between request to general planner and its successful (200 OK) response.
-- `pg.delivery_requests` - total number of requests to delivery stats service.
-- `pg.delivery_request_failed` - number of failed requests to delivery stats service.
-- `pg.delivery_request_successful` - number of successful requests to delivery stats service.
-- `pg.delivery_request_time` - latency between request to delivery stats and its successful (200 OK) response.
diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml
index b48eaf7780d..5f17d237be8 100644
--- a/extra/bundle/pom.xml
+++ b/extra/bundle/pom.xml
@@ -5,7 +5,7 @@
org.prebid
prebid-server-aggregator
- 2.13.0-SNAPSHOT
+ 3.15.0-SNAPSHOT
../../extra/pom.xml
@@ -14,15 +14,6 @@
prebid-server-bundle
Creates bundle (fat jar) with PBS-Core and other submodules listed as dependency
-
- UTF-8
- UTF-8
- 17
- ${java.version}
- ${java.version}
- 2.5.6
-
-
org.prebid
@@ -34,6 +25,11 @@
confiant-ad-quality
${project.version}
+
+ org.prebid.server.hooks.modules
+ fiftyone-devicedetection
+ ${project.version}
+
org.prebid.server.hooks.modules
ortb2-blocking
@@ -44,6 +40,11 @@
pb-richmedia-filter
${project.version}
+
+ org.prebid.server.hooks.modules
+ pb-response-correction
+ ${project.version}
+
diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml
index e04ca09ea57..a4b77048c76 100644
--- a/extra/modules/confiant-ad-quality/pom.xml
+++ b/extra/modules/confiant-ad-quality/pom.xml
@@ -5,7 +5,7 @@
org.prebid.server.hooks.modules
all-modules
- 2.13.0-SNAPSHOT
+ 3.15.0-SNAPSHOT
confiant-ad-quality
@@ -17,7 +17,6 @@
io.vertx
vertx-redis-client
- 3.9.10
diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisClient.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisClient.java
index f03d07ca33c..d4c9864dd9c 100644
--- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisClient.java
+++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisClient.java
@@ -4,13 +4,13 @@
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.core.Vertx;
-import io.vertx.core.logging.Logger;
-import io.vertx.core.logging.LoggerFactory;
import io.vertx.redis.client.Redis;
import io.vertx.redis.client.RedisAPI;
import io.vertx.redis.client.RedisConnection;
import io.vertx.redis.client.RedisOptions;
import org.prebid.server.hooks.modules.com.confiant.adquality.model.RedisRetryConfig;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
public class RedisClient {
@@ -45,7 +45,7 @@ public RedisClient(
public void start(Promise startFuture) {
createRedisClient(onCreate -> {
if (onCreate.succeeded()) {
- logger.info("Confiant Redis {0} connection is established", type);
+ logger.info("Confiant Redis {} connection is established", type);
startFuture.tryComplete();
}
}, false);
diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisParser.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisParser.java
index a516497146d..11cabafbeb0 100644
--- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisParser.java
+++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisParser.java
@@ -2,10 +2,10 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
-import io.vertx.core.logging.Logger;
-import io.vertx.core.logging.LoggerFactory;
import org.prebid.server.hooks.modules.com.confiant.adquality.model.BidScanResult;
import org.prebid.server.hooks.modules.com.confiant.adquality.model.RedisError;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
import java.util.Arrays;
import java.util.Collections;
diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java
index 4cf66880bef..7db1446bcce 100644
--- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java
+++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java
@@ -81,7 +81,7 @@ private BidRequest getBidRequest(AuctionInvocationContext auctionInvocationConte
final boolean disallowTransmitGeo = !auctionContext.getActivityInfrastructure()
.isAllowed(Activity.TRANSMIT_GEO, activityInvocationPayload);
- final User maskedUser = userFpdActivityMask.maskUser(bidRequest.getUser(), true, true, disallowTransmitGeo);
+ final User maskedUser = userFpdActivityMask.maskUser(bidRequest.getUser(), true, true);
final Device maskedDevice = userFpdActivityMask.maskDevice(bidRequest.getDevice(), true, disallowTransmitGeo);
return bidRequest.toBuilder()
diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java
index 0a017e08df1..9ec01a7cfed 100644
--- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java
+++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java
@@ -1,6 +1,6 @@
package org.prebid.server.hooks.modules.com.confiant.adquality.core;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.prebid.server.auction.model.BidderResponse;
import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils;
import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl;
diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsMapperTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsMapperTest.java
index e0e1405403f..e60f3beaea4 100644
--- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsMapperTest.java
+++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsMapperTest.java
@@ -3,7 +3,7 @@
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.response.SeatBid;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.prebid.server.auction.model.BidderResponse;
import org.prebid.server.hooks.modules.com.confiant.adquality.model.RedisBidResponseData;
import org.prebid.server.hooks.modules.com.confiant.adquality.model.RedisBidsData;
diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScanResultTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScanResultTest.java
index 7ebc109907e..335d00527c2 100644
--- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScanResultTest.java
+++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScanResultTest.java
@@ -1,7 +1,7 @@
package org.prebid.server.hooks.modules.com.confiant.adquality.core;
import com.fasterxml.jackson.databind.ObjectMapper;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import org.prebid.server.auction.model.BidderResponse;
import org.prebid.server.hooks.modules.com.confiant.adquality.model.GroupByIssues;
import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils;
diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScannerTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScannerTest.java
index bd436d81f61..7138dd536e1 100644
--- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScannerTest.java
+++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/BidsScannerTest.java
@@ -9,12 +9,11 @@
import io.vertx.redis.client.RedisAPI;
import io.vertx.redis.client.Response;
import io.vertx.redis.client.ResponseType;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+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.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.prebid.server.auction.model.BidderResponse;
import org.prebid.server.hooks.modules.com.confiant.adquality.model.GroupByIssues;
import org.prebid.server.hooks.modules.com.confiant.adquality.model.RedisBidResponseData;
@@ -28,11 +27,9 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
+@ExtendWith(MockitoExtension.class)
public class BidsScannerTest {
- @Rule
- public final MockitoRule mockitoRule = MockitoJUnit.rule();
-
@Mock
private RedisClient writeRedisNode;
@@ -44,7 +41,7 @@ public class BidsScannerTest {
private BidsScanner bidsScannerTest;
- @Before
+ @BeforeEach
public void setUp() {
bidsScannerTest = new BidsScanner(writeRedisNode, readRedisNode, "api-key", new ObjectMapper());
}
diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisParserTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisParserTest.java
index 3fdf2e62236..ab1bfe12516 100644
--- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisParserTest.java
+++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisParserTest.java
@@ -1,7 +1,7 @@
package org.prebid.server.hooks.modules.com.confiant.adquality.core;
import com.fasterxml.jackson.databind.ObjectMapper;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisScanStateCheckerTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisScanStateCheckerTest.java
index a7f5b10ca08..b42d3e38fa3 100644
--- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisScanStateCheckerTest.java
+++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/RedisScanStateCheckerTest.java
@@ -2,28 +2,25 @@
import io.vertx.core.Future;
import io.vertx.core.Vertx;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+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.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
+import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+@ExtendWith(MockitoExtension.class)
public class RedisScanStateCheckerTest {
- @Rule
- public final MockitoRule mockitoRule = MockitoJUnit.rule();
-
@Mock
private BidsScanner bidsScanner;
private RedisScanStateChecker scanStateChecker;
- @Before
+ @BeforeEach
public void setUp() {
scanStateChecker = new RedisScanStateChecker(bidsScanner, 1000L, Vertx.vertx());
}
diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java
index 8746a3e10b2..d4b39a8214e 100644
--- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java
+++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java
@@ -6,12 +6,11 @@
import com.iab.openrtb.request.Geo;
import com.iab.openrtb.request.User;
import io.vertx.core.Future;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
+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.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
+import org.mockito.junit.jupiter.MockitoExtension;
import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
import org.prebid.server.auction.model.AuctionContext;
import org.prebid.server.auction.model.BidderResponse;
@@ -43,11 +42,9 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
+@ExtendWith(MockitoExtension.class)
public class ConfiantAdQualityBidResponsesScanHookTest {
- @Rule
- public final MockitoRule mockitoRule = MockitoJUnit.rule();
-
@Mock
private BidsScanner bidsScanner;
@@ -67,18 +64,14 @@ public class ConfiantAdQualityBidResponsesScanHookTest {
private final RedisParser redisParser = new RedisParser(new ObjectMapper());
- @Before
+ @BeforeEach
public void setUp() {
target = new ConfiantAdQualityBidResponsesScanHook(bidsScanner, List.of(), userFpdActivityMask);
}
@Test
public void codeShouldHaveValidConfigsWhenInitialized() {
- // given
-
- // when
-
- // then
+ // when and then
assertThat(target.code()).isEqualTo("confiant-ad-quality-bid-responses-scan-hook");
}
@@ -277,8 +270,7 @@ public void callShouldSubmitBidsWithoutMaskedGeoInfoWhenTransmitGeoIsAllowed() {
final Boolean transmitGeoIsAllowed = true;
final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult(
"[[[{\"tag_key\": \"tag\", \"issues\":[{\"spec_name\":\"malicious_domain\",\"value\":\"ads.deceivenetworks.net\",\"first_adinstance\":\"e91e8da982bb8b7f80100426\"}]}]]]");
- final User user = userFpdActivityMask.maskUser(
- getUser(), true, true, !transmitGeoIsAllowed);
+ final User user = userFpdActivityMask.maskUser(getUser(), true, true);
final Device device = userFpdActivityMask.maskDevice(
getDevice(), true, !transmitGeoIsAllowed);
@@ -306,8 +298,7 @@ public void callShouldSubmitBidsWithMaskedGeoInfoWhenTransmitGeoIsNotAllowed() {
final Boolean transmitGeoIsAllowed = false;
final BidsScanResult bidsScanResult = redisParser.parseBidsScanResult(
"[[[{\"tag_key\": \"tag\", \"issues\":[{\"spec_name\":\"malicious_domain\",\"value\":\"ads.deceivenetworks.net\",\"first_adinstance\":\"e91e8da982bb8b7f80100426\"}]}]]]");
- final User user = userFpdActivityMask.maskUser(
- getUser(), true, true, !transmitGeoIsAllowed);
+ final User user = userFpdActivityMask.maskUser(getUser(), true, true);
final Device device = userFpdActivityMask.maskDevice(
getDevice(), true, !transmitGeoIsAllowed);
diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java
index 16fe689b6ff..41e63920319 100644
--- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java
+++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityModuleTest.java
@@ -1,6 +1,6 @@
package org.prebid.server.hooks.modules.com.confiant.adquality.v1;
-import org.junit.Test;
+import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
@@ -8,11 +8,7 @@ public class ConfiantAdQualityModuleTest {
@Test
public void shouldHaveValidInitialConfigs() {
- // given
-
- // when
-
- // then
+ // when and then
assertThat(ConfiantAdQualityModule.CODE).isEqualTo("confiant-ad-quality");
}
}
diff --git a/extra/modules/fiftyone-devicedetection/README.md b/extra/modules/fiftyone-devicedetection/README.md
new file mode 100644
index 00000000000..fbe254b28c1
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/README.md
@@ -0,0 +1,181 @@
+# Overview
+
+51Degrees module enriches an incoming OpenRTB request [51Degrees Device Data](https://51degrees.com/documentation/_device_detection__overview.html).
+
+51Degrees module sets the following fields of the device object: `make`, `model`, `os`, `osv`, `h`, `w`, `ppi`, `pixelratio` - interested bidder adapters may use these fields as needed. In addition the module sets `device.ext.fiftyonedegrees_deviceId` to a permanent device ID which can be rapidly looked up in on premise data exposing over 250 properties including the device age, chip set, codec support, and price, operating system and app/browser versions, age, and embedded features.
+
+## Setup
+
+The 51Degrees module operates using a data file. You can get started with a free Lite data file that can be downloaded here: [https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash](https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash). The Lite file is capable of detecting limited device information, so if you need in-depth device data, please contact 51Degrees to obtain a license: [https://51degrees.com/contact-us](https://51degrees.com/contact-us?ContactReason=Free%20Trial).
+
+Put the data file in a file system location writable by the user that is running the Prebid Server module and specify that directory location in the configuration parameters. The location needs to be writable if you would like to enable [automatic data file updates](https://51degrees.com/documentation/_features__automatic_datafile_updates.html).
+
+## Configuration
+
+To start using current module you have to enable module and add `fiftyone-devicedetection-entrypoint-hook` and `fiftyone-devicedetection-raw-auction-request-hook` into hooks execution plan inside your yaml file:
+
+```yaml
+hooks:
+ fiftyone-devicedetection:
+ enabled: true
+ host-execution-plan: >
+ {
+ "endpoints": {
+ "/openrtb2/auction": {
+ "stages": {
+ "entrypoint": {
+ "groups": [
+ {
+ "timeout": 100,
+ "hook-sequence": [
+ {
+ "module-code": "fiftyone-devicedetection",
+ "hook-impl-code": "fiftyone-devicedetection-entrypoint-hook"
+ }
+ ]
+ }
+ ]
+ },
+ "raw-auction-request": {
+ "groups": [
+ {
+ "timeout": 100,
+ "hook-sequence": [
+ {
+ "module-code": "fiftyone-devicedetection",
+ "hook-impl-code": "fiftyone-devicedetection-raw-auction-request-hook"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+```
+
+And configure
+
+## List of module configuration options
+
+- `account-filter`
+ - `allow-list` - _(list of strings)_ - A list of account IDs that are allowed to use this module. If empty, everyone is allowed. Full-string match is performed (whitespaces and capitalization matter). Defaults to empty.
+- `data-file`
+ - `path` - _(string, **REQUIRED**)_ - The full path to the device detection data file. Sample file can be downloaded from [[data repo on GitHub](https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash)].
+ - `make-temp-copy` - _(boolean)_ - If true, the engine will create a temporary copy of the data file rather than using the data file directly. Defaults to false.
+ - `update`
+ - `auto` - _(boolean)_ - Enable/Disable auto update. Defaults to enabled. If enabled, the auto update system will automatically download and apply new data files for device detection.
+ - `on-startup` - _(boolean)_ - Enable/Disable update on startup. Defaults to enabled. If enabled, the auto update system will be used to check for an update before the device detection engine is created. If an update is available, it will be downloaded and applied before the pipeline is built and returned for use so this may take some time.
+ - `url` - _(string)_ - Configure the engine to use the specified URL when looking for an updated data file. Default is the 51Degrees update URL.
+ - `license-key` - _(string)_ - Set the license key used when checking for new device detection data files. Defaults to null.
+ - `watch-file-system` - _(boolean)_ - The DataUpdateService has the ability to watch a file on disk and refresh the engine as soon as that file is updated. This setting enables/disables that feature. Defaults to true.
+ - `polling-interval` - _(int, seconds)_ - Set the time between checks for a new data file made by the DataUpdateService in seconds. Default = 30 minutes.
+- `performance`
+ - `profile` - _(string)_ - Set the performance profile for the device detection engine. Must be one of: LowMemory, MaxPerformance, HighPerformance, Balanced, BalancedTemp. Defaults to balanced.
+ - `concurrency` - _(int)_ - Set the expected number of concurrent operations using the engine. This sets the concurrency of the internal caches to avoid excessive locking. Default: 10.
+ - `difference` - _(int)_ - Set the maximum difference to allow when processing HTTP headers. The meaning of difference depends on the Device Detection API being used. The difference is the difference in hash value between the hash that was found, and the hash that is being searched for. By default this is 0. For more information see [51Degrees documentation](https://51degrees.com/documentation/_device_detection__hash.html).
+ - `allow-unmatched` - _(boolean)_ - If set to false, a non-matching User-Agent will result in properties without set values.
+ If set to true, a non-matching User-Agent will cause the 'default profiles' to be returned. This means that properties will always have values (i.e. no need to check .hasValue) but some may be inaccurate. By default, this is false.
+ - `drift` - _(int)_ - Set the maximum drift to allow when matching hashes. If the drift is exceeded, the result is considered invalid and values will not be returned. By default this is 0. For more information see [51Degrees documentation](https://51degrees.com/documentation/_device_detection__hash.html).
+
+```yaml
+hooks:
+ modules:
+ fiftyone-devicedetection:
+ account-filter:
+ allow-list: [] # list of strings, account ids for enabled publishers, or empty for all
+ data-file:
+ path: ~ # string, REQUIRED, download the sample from https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash or Enterprise from https://51degrees.com/pricing
+ make-temp-copy: ~ # boolean
+ update:
+ auto: ~ # boolean
+ on-startup: ~ # boolean
+ url: ~ # string
+ license-key: ~ # string
+ watch-file-system: ~ # boolean
+ polling-interval: ~ # int, seconds
+ performance:
+ profile: ~ # string, one of [LowMemory,MaxPerformance,HighPerformance,Balanced,BalancedTemp]
+ concurrency: ~ # int
+ difference: ~ # int
+ allow-unmatched: ~ # boolean
+ drift: ~ # int
+```
+
+Minimal sample (only required):
+
+```yaml
+ modules:
+ fiftyone-devicedetection:
+ data-file:
+ path: "51Degrees-LiteV4.1.hash" # string, REQUIRED, download the sample from https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash or Enterprise from https://51degrees.com/pricing
+```
+
+## Running the demo
+
+1. Build the server bundle JAR as described in [[Build Project](../../../docs/build.md#build-project)], e.g.
+
+```bash
+mvn clean package --file extra/pom.xml
+```
+
+2. Download `51Degrees-LiteV4.1.hash` from [[GitHub](https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash)] and put it in the project root directory.
+
+```bash
+curl -o 51Degrees-LiteV4.1.hash -L https://github.com/51Degrees/device-detection-data/raw/main/51Degrees-LiteV4.1.hash
+```
+
+3. Start server bundle JAR as described in [[Running project](../../../docs/run.md#running-project)], e.g.
+
+```bash
+java -jar target/prebid-server-bundle.jar --spring.config.additional-location=sample/prebid-config-with-51d-dd.yaml
+```
+
+4. Run sample request against the server as described in [[requests/README](../../../sample/requests/README.txt)], e.g.
+
+```bash
+curl http://localhost:8080/openrtb2/auction --data @extra/modules/fiftyone-devicedetection/sample-requests/data.json
+```
+
+5. See the `device` object be enriched
+
+```diff
+ "device": {
+- "ua": "Mozilla/5.0 (Linux; Android 11; SM-G998W) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36"
++ "ua": "Mozilla/5.0 (Linux; Android 11; SM-G998W) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36",
++ "os": "Android",
++ "osv": "11.0",
++ "h": 3200,
++ "w": 1440,
++ "ext": {
++ "fiftyonedegrees_deviceId": "110698-102757-105219-0"
++ }
+ },
+```
+
+[[Enterprise](https://51degrees.com/pricing)] files can provide even more information:
+
+```diff
+ "device": {
+ "ua": "Mozilla/5.0 (Linux; Android 11; SM-G998W) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36",
++ "devicetype": 1,
++ "make": "Samsung",
++ "model": "SM-G998W",
+ "os": "Android",
+ "osv": "11.0",
+ "h": 3200,
+ "w": 1440,
++ "ppi": 516,
++ "pxratio": 3.44,
+ "ext": {
+- "fiftyonedegrees_deviceId": "110698-102757-105219-0"
++ "fiftyonedegrees_deviceId": "110698-102757-105219-18092"
+ }
+```
+
+## Maintainer contacts
+
+Any suggestions or questions can be directed to [support@51degrees.com](support@51degrees.com) e-mail.
+
+Or just open new [issue](https://github.com/prebid/prebid-server-java/issues/new) or [pull request](https://github.com/prebid/prebid-server-java/pulls) in this repository.
\ No newline at end of file
diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml
new file mode 100644
index 00000000000..963b239763e
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/pom.xml
@@ -0,0 +1,49 @@
+
+
+ 4.0.0
+
+
+ org.prebid.server.hooks.modules
+ all-modules
+ 3.15.0-SNAPSHOT
+
+
+ fiftyone-devicedetection
+
+ fiftyone-devicedetection
+ 51Degrees Device Detection module
+
+
+ 4.4.94
+ 1.2.13
+
+
+
+
+
+ com.51degrees
+ device-detection.hash.engine.on-premise
+ ${fiftyone-device-detection.version}
+
+
+
+
+ com.51degrees
+ device-detection
+ ${fiftyone-device-detection.version}
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+ test
+
+
+ ch.qos.logback
+ logback-core
+ ${logback.version}
+ test
+
+
+
diff --git a/extra/modules/fiftyone-devicedetection/sample-requests/data.json b/extra/modules/fiftyone-devicedetection/sample-requests/data.json
new file mode 100644
index 00000000000..c87b9876553
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/sample-requests/data.json
@@ -0,0 +1,146 @@
+{
+ "imp":
+ [
+ {
+ "ext":
+ {
+ "data":
+ {
+ "adserver":
+ {
+ "name": "gam",
+ "adslot": "test"
+ },
+ "pbadslot": "test",
+ "gpid": "test"
+ },
+ "gpid": "test",
+ "prebid":
+ {
+ "bidder":
+ {
+ "appnexus":
+ {
+ "placement_id": 1,
+ "use_pmt_rule": false
+ }
+ },
+ "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": "1"
+ },
+ "page": "https://www.test.com/"
+ },
+ "device":
+ {
+ "ua": "Mozilla/5.0 (Linux; Android 11; SM-G998W) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Mobile Safari/537.36"
+ },
+ "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": 1698390609882,
+ "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": 1700
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/config/FiftyOneDeviceDetectionModuleConfiguration.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/config/FiftyOneDeviceDetectionModuleConfiguration.java
new file mode 100644
index 00000000000..175bc5db1dc
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/config/FiftyOneDeviceDetectionModuleConfiguration.java
@@ -0,0 +1,49 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.config;
+
+import fiftyone.devicedetection.DeviceDetectionPipelineBuilder;
+import fiftyone.pipeline.core.flowelements.Pipeline;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.FiftyOneDeviceDetectionModule;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.DeviceEnricher;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.PipelineBuilder;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.hooks.FiftyOneDeviceDetectionEntrypointHook;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.hooks.FiftyOneDeviceDetectionRawAuctionRequestHook;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config.ModuleConfig;
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationContext;
+import org.prebid.server.hooks.v1.Module;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Set;
+
+@Configuration
+@ConditionalOnProperty(prefix = "hooks." + FiftyOneDeviceDetectionModule.CODE, name = "enabled", havingValue = "true")
+public class FiftyOneDeviceDetectionModuleConfiguration {
+ @Bean
+ @ConfigurationProperties(prefix = "hooks.modules." + FiftyOneDeviceDetectionModule.CODE)
+ ModuleConfig moduleConfig() {
+ return new ModuleConfig();
+ }
+
+ @Bean
+ Pipeline pipeline(ModuleConfig moduleConfig) throws Exception {
+ return new PipelineBuilder(moduleConfig).build(new DeviceDetectionPipelineBuilder());
+ }
+
+ @Bean
+ DeviceEnricher deviceEnricher(Pipeline pipeline) {
+ return new DeviceEnricher(pipeline);
+ }
+
+ @Bean
+ Module fiftyOneDeviceDetectionModule(ModuleConfig moduleConfig, DeviceEnricher deviceEnricher) {
+ final Set extends Hook, ? extends InvocationContext>> hooks = Set.of(
+ new FiftyOneDeviceDetectionEntrypointHook(),
+ new FiftyOneDeviceDetectionRawAuctionRequestHook(moduleConfig.getAccountFilter(), deviceEnricher)
+ );
+
+ return new FiftyOneDeviceDetectionModule(hooks);
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/boundary/CollectedEvidence.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/boundary/CollectedEvidence.java
new file mode 100644
index 00000000000..d6ed6ab4f53
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/boundary/CollectedEvidence.java
@@ -0,0 +1,14 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.boundary;
+
+import lombok.Builder;
+
+import java.util.Collection;
+import java.util.Map;
+
+@Builder(toBuilder = true)
+public record CollectedEvidence(
+ Collection> rawHeaders,
+ String deviceUA,
+ Map secureHeaders
+) {
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/AccountFilter.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/AccountFilter.java
new file mode 100644
index 00000000000..20b22cc4e3d
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/AccountFilter.java
@@ -0,0 +1,10 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public final class AccountFilter {
+ List allowList;
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFile.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFile.java
new file mode 100644
index 00000000000..6cc0dc64b7e
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFile.java
@@ -0,0 +1,10 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config;
+
+import lombok.Data;
+
+@Data
+public final class DataFile {
+ String path;
+ Boolean makeTempCopy;
+ DataFileUpdate update;
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileUpdate.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileUpdate.java
new file mode 100644
index 00000000000..2ae0655c59c
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileUpdate.java
@@ -0,0 +1,13 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config;
+
+import lombok.Data;
+
+@Data
+public final class DataFileUpdate {
+ Boolean auto;
+ Boolean onStartup;
+ String url;
+ String licenseKey;
+ Boolean watchFileSystem;
+ Integer pollingInterval;
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/ModuleConfig.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/ModuleConfig.java
new file mode 100644
index 00000000000..9783317ce5c
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/ModuleConfig.java
@@ -0,0 +1,10 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config;
+
+import lombok.Data;
+
+@Data
+public final class ModuleConfig {
+ AccountFilter accountFilter;
+ DataFile dataFile;
+ PerformanceConfig performance;
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/PerformanceConfig.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/PerformanceConfig.java
new file mode 100644
index 00000000000..088e25eae34
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/PerformanceConfig.java
@@ -0,0 +1,12 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config;
+
+import lombok.Data;
+
+@Data
+public final class PerformanceConfig {
+ String profile;
+ Integer concurrency;
+ Integer difference;
+ Boolean allowUnmatched;
+ Integer drift;
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/FiftyOneDeviceDetectionModule.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/FiftyOneDeviceDetectionModule.java
new file mode 100644
index 00000000000..5bc2b8e82ab
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/FiftyOneDeviceDetectionModule.java
@@ -0,0 +1,23 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1;
+
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationContext;
+import org.prebid.server.hooks.v1.Module;
+
+import java.util.Collection;
+
+public record FiftyOneDeviceDetectionModule(
+ Collection extends Hook, ? extends InvocationContext>> hooks
+) implements Module {
+ public static final String CODE = "fiftyone-devicedetection";
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+
+ @Override
+ public Collection extends Hook, ? extends InvocationContext>> hooks() {
+ return hooks;
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/DeviceEnricher.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/DeviceEnricher.java
new file mode 100644
index 00000000000..8b34666efbf
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/DeviceEnricher.java
@@ -0,0 +1,327 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.iab.openrtb.request.Device;
+import fiftyone.devicedetection.shared.DeviceData;
+import fiftyone.pipeline.core.data.FlowData;
+import fiftyone.pipeline.core.flowelements.Pipeline;
+import fiftyone.pipeline.engines.data.AspectPropertyValue;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.boundary.CollectedEvidence;
+import org.prebid.server.model.UpdateResult;
+import org.prebid.server.proto.openrtb.ext.request.ExtDevice;
+
+import jakarta.annotation.Nonnull;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class DeviceEnricher {
+ private static final String EXT_DEVICE_ID_KEY = "fiftyonedegrees_deviceId";
+
+ private final Pipeline pipeline;
+
+ public DeviceEnricher(@Nonnull Pipeline pipeline) {
+ this.pipeline = Objects.requireNonNull(pipeline);
+ }
+
+ public static boolean shouldSkipEnriching(Device device) {
+ return StringUtils.isNotEmpty(getDeviceId(device));
+ }
+
+ public EnrichmentResult populateDeviceInfo(Device device, CollectedEvidence collectedEvidence) throws Exception {
+ try (FlowData data = pipeline.createFlowData()) {
+ data.addEvidence(pickRelevantFrom(collectedEvidence));
+ data.process();
+ final DeviceData deviceData = data.get(DeviceData.class);
+ if (deviceData == null) {
+ return null;
+ }
+ final Device properDevice = Optional.ofNullable(device).orElseGet(() -> Device.builder().build());
+ return patchDevice(properDevice, deviceData);
+ }
+ }
+
+ private Map pickRelevantFrom(CollectedEvidence collectedEvidence) {
+ final Map evidence = new HashMap<>();
+
+ final String ua = collectedEvidence.deviceUA();
+ if (StringUtils.isNotBlank(ua)) {
+ evidence.put("header.user-agent", ua);
+ }
+ final Map secureHeaders = collectedEvidence.secureHeaders();
+ if (MapUtils.isNotEmpty(secureHeaders)) {
+ evidence.putAll(secureHeaders);
+ }
+ if (!evidence.isEmpty()) {
+ return evidence;
+ }
+
+ Stream.ofNullable(collectedEvidence.rawHeaders())
+ .flatMap(Collection::stream)
+ .forEach(rawHeader -> evidence.put("header." + rawHeader.getKey(), rawHeader.getValue()));
+
+ return evidence;
+ }
+
+ private EnrichmentResult patchDevice(Device device, DeviceData deviceData) {
+ final List updatedFields = new ArrayList<>();
+ final Device.DeviceBuilder deviceBuilder = device.toBuilder();
+
+ final UpdateResult resolvedDeviceType = resolveDeviceType(device, deviceData);
+ if (resolvedDeviceType.isUpdated()) {
+ deviceBuilder.devicetype(resolvedDeviceType.getValue());
+ updatedFields.add("devicetype");
+ }
+
+ final UpdateResult resolvedMake = resolveMake(device, deviceData);
+ if (resolvedMake.isUpdated()) {
+ deviceBuilder.make(resolvedMake.getValue());
+ updatedFields.add("make");
+ }
+
+ final UpdateResult resolvedModel = resolveModel(device, deviceData);
+ if (resolvedModel.isUpdated()) {
+ deviceBuilder.model(resolvedModel.getValue());
+ updatedFields.add("model");
+ }
+
+ final UpdateResult resolvedOs = resolveOs(device, deviceData);
+ if (resolvedOs.isUpdated()) {
+ deviceBuilder.os(resolvedOs.getValue());
+ updatedFields.add("os");
+ }
+
+ final UpdateResult resolvedOsv = resolveOsv(device, deviceData);
+ if (resolvedOsv.isUpdated()) {
+ deviceBuilder.osv(resolvedOsv.getValue());
+ updatedFields.add("osv");
+ }
+
+ final UpdateResult resolvedH = resolveH(device, deviceData);
+ if (resolvedH.isUpdated()) {
+ deviceBuilder.h(resolvedH.getValue());
+ updatedFields.add("h");
+ }
+
+ final UpdateResult resolvedW = resolveW(device, deviceData);
+ if (resolvedW.isUpdated()) {
+ deviceBuilder.w(resolvedW.getValue());
+ updatedFields.add("w");
+ }
+
+ final UpdateResult resolvedPpi = resolvePpi(device, deviceData);
+ if (resolvedPpi.isUpdated()) {
+ deviceBuilder.ppi(resolvedPpi.getValue());
+ updatedFields.add("ppi");
+ }
+
+ final UpdateResult resolvedPixelRatio = resolvePixelRatio(device, deviceData);
+ if (resolvedPixelRatio.isUpdated()) {
+ deviceBuilder.pxratio(resolvedPixelRatio.getValue());
+ updatedFields.add("pxratio");
+ }
+
+ final UpdateResult resolvedDeviceId = resolveDeviceId(device, deviceData);
+ if (resolvedDeviceId.isUpdated()) {
+ setDeviceId(deviceBuilder, device, resolvedDeviceId.getValue());
+ updatedFields.add("ext." + EXT_DEVICE_ID_KEY);
+ }
+
+ if (updatedFields.isEmpty()) {
+ return null;
+ }
+
+ return EnrichmentResult.builder()
+ .enrichedDevice(deviceBuilder.build())
+ .enrichedFields(updatedFields)
+ .build();
+ }
+
+ private UpdateResult resolveDeviceType(Device device, DeviceData deviceData) {
+ final Integer currentDeviceType = device.getDevicetype();
+ if (isPositive(currentDeviceType)) {
+ return UpdateResult.unaltered(currentDeviceType);
+ }
+
+ final String rawDeviceType = getSafe(deviceData, DeviceData::getDeviceType);
+ if (rawDeviceType == null) {
+ return UpdateResult.unaltered(currentDeviceType);
+ }
+
+ final OrtbDeviceType properDeviceType = OrtbDeviceType.resolveFrom(rawDeviceType);
+ return properDeviceType != OrtbDeviceType.UNKNOWN
+ ? UpdateResult.updated(properDeviceType.ordinal())
+ : UpdateResult.unaltered(currentDeviceType);
+ }
+
+ private UpdateResult resolveMake(Device device, DeviceData deviceData) {
+ final String currentMake = device.getMake();
+ if (StringUtils.isNotBlank(currentMake)) {
+ return UpdateResult.unaltered(currentMake);
+ }
+
+ final String make = getSafe(deviceData, DeviceData::getHardwareVendor);
+ return StringUtils.isNotBlank(make)
+ ? UpdateResult.updated(make)
+ : UpdateResult.unaltered(currentMake);
+ }
+
+ private UpdateResult resolveModel(Device device, DeviceData deviceData) {
+ final String currentModel = device.getModel();
+ if (StringUtils.isNotBlank(currentModel)) {
+ return UpdateResult.unaltered(currentModel);
+ }
+
+ final String model = getSafe(deviceData, DeviceData::getHardwareModel);
+ if (StringUtils.isNotBlank(model)) {
+ return UpdateResult.updated(model);
+ }
+
+ final List names = getSafe(deviceData, DeviceData::getHardwareName);
+ return CollectionUtils.isNotEmpty(names)
+ ? UpdateResult.updated(String.join(",", names))
+ : UpdateResult.unaltered(currentModel);
+ }
+
+ private UpdateResult resolveOs(Device device, DeviceData deviceData) {
+ final String currentOs = device.getOs();
+ if (StringUtils.isNotBlank(currentOs)) {
+ return UpdateResult.unaltered(currentOs);
+ }
+
+ final String os = getSafe(deviceData, DeviceData::getPlatformName);
+ return StringUtils.isNotBlank(os)
+ ? UpdateResult.updated(os)
+ : UpdateResult.unaltered(currentOs);
+ }
+
+ private UpdateResult resolveOsv(Device device, DeviceData deviceData) {
+ final String currentOsv = device.getOsv();
+ if (StringUtils.isNotBlank(currentOsv)) {
+ return UpdateResult.unaltered(currentOsv);
+ }
+
+ final String osv = getSafe(deviceData, DeviceData::getPlatformVersion);
+ return StringUtils.isNotBlank(osv)
+ ? UpdateResult.updated(osv)
+ : UpdateResult.unaltered(currentOsv);
+ }
+
+ private UpdateResult resolveH(Device device, DeviceData deviceData) {
+ final Integer currentH = device.getH();
+ if (isPositive(currentH)) {
+ return UpdateResult.unaltered(currentH);
+ }
+
+ final Integer h = getSafe(deviceData, DeviceData::getScreenPixelsHeight);
+ return isPositive(h)
+ ? UpdateResult.updated(h)
+ : UpdateResult.unaltered(currentH);
+ }
+
+ private UpdateResult resolveW(Device device, DeviceData deviceData) {
+ final Integer currentW = device.getW();
+ if (isPositive(currentW)) {
+ return UpdateResult.unaltered(currentW);
+ }
+
+ final Integer w = getSafe(deviceData, DeviceData::getScreenPixelsWidth);
+ return isPositive(w)
+ ? UpdateResult.updated(w)
+ : UpdateResult.unaltered(currentW);
+ }
+
+ private UpdateResult resolvePpi(Device device, DeviceData deviceData) {
+ final Integer currentPpi = device.getPpi();
+ if (isPositive(currentPpi)) {
+ return UpdateResult.unaltered(currentPpi);
+ }
+
+ final Integer pixelsHeight = getSafe(deviceData, DeviceData::getScreenPixelsHeight);
+ if (pixelsHeight == null) {
+ return UpdateResult.unaltered(currentPpi);
+ }
+
+ final Double inchesHeight = getSafe(deviceData, DeviceData::getScreenInchesHeight);
+ return isPositive(inchesHeight)
+ ? UpdateResult.updated((int) Math.round(pixelsHeight / inchesHeight))
+ : UpdateResult.unaltered(currentPpi);
+ }
+
+ private UpdateResult resolvePixelRatio(Device device, DeviceData deviceData) {
+ final BigDecimal currentPixelRatio = device.getPxratio();
+ if (currentPixelRatio != null && currentPixelRatio.intValue() > 0) {
+ return UpdateResult.unaltered(currentPixelRatio);
+ }
+
+ final Double rawRatio = getSafe(deviceData, DeviceData::getPixelRatio);
+ return isPositive(rawRatio)
+ ? UpdateResult.updated(BigDecimal.valueOf(rawRatio))
+ : UpdateResult.unaltered(currentPixelRatio);
+ }
+
+ private UpdateResult resolveDeviceId(Device device, DeviceData deviceData) {
+ final String currentDeviceId = getDeviceId(device);
+ if (StringUtils.isNotBlank(currentDeviceId)) {
+ return UpdateResult.unaltered(currentDeviceId);
+ }
+
+ final String deviceID = getSafe(deviceData, DeviceData::getDeviceId);
+ return StringUtils.isNotBlank(deviceID)
+ ? UpdateResult.updated(deviceID)
+ : UpdateResult.unaltered(currentDeviceId);
+ }
+
+ private static boolean isPositive(Integer value) {
+ return value != null && value > 0;
+ }
+
+ private static boolean isPositive(Double value) {
+ return value != null && value > 0;
+ }
+
+ private static String getDeviceId(Device device) {
+ final ExtDevice ext = device.getExt();
+ if (ext == null) {
+ return null;
+ }
+ final JsonNode savedValue = ext.getProperty(EXT_DEVICE_ID_KEY);
+ return (savedValue != null && savedValue.isTextual()) ? savedValue.textValue() : null;
+ }
+
+ private static void setDeviceId(Device.DeviceBuilder deviceBuilder, Device device, String deviceId) {
+ ExtDevice ext = null;
+ if (device != null) {
+ ext = device.getExt();
+ }
+ if (ext == null) {
+ ext = ExtDevice.empty();
+ }
+ ext.addProperty(EXT_DEVICE_ID_KEY, new TextNode(deviceId));
+ deviceBuilder.ext(ext);
+ }
+
+ private T getSafe(DeviceData deviceData, Function> propertyGetter) {
+ try {
+ final AspectPropertyValue propertyValue = propertyGetter.apply(deviceData);
+ if (propertyValue != null && propertyValue.hasValue()) {
+ return propertyValue.getValue();
+ }
+ } catch (Exception e) {
+ // nop -- not interested in errors on getting missing values.
+ }
+ return null;
+ }
+}
+
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/EnrichmentResult.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/EnrichmentResult.java
new file mode 100644
index 00000000000..237846d679b
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/EnrichmentResult.java
@@ -0,0 +1,13 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core;
+
+import com.iab.openrtb.request.Device;
+import lombok.Builder;
+
+import java.util.Collection;
+
+@Builder
+public record EnrichmentResult(
+ Device enrichedDevice,
+ Collection enrichedFields
+) {
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/OrtbDeviceType.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/OrtbDeviceType.java
new file mode 100644
index 00000000000..078279fb2a3
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/OrtbDeviceType.java
@@ -0,0 +1,39 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core;
+
+import java.util.Map;
+import java.util.Optional;
+
+// https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/main/AdCOM%20v1.0%20FINAL.md#list--device-types-
+public enum OrtbDeviceType {
+ UNKNOWN,
+ MOBILE_TABLET,
+ PERSONAL_COMPUTER,
+ CONNECTED_TV,
+ PHONE,
+ TABLET,
+ CONNECTED_DEVICE,
+ SET_TOP_BOX,
+ OOH_DEVICE;
+
+ private static final Map DEVICE_FIELD_MAPPING = Map.ofEntries(
+ Map.entry("Phone", OrtbDeviceType.PHONE),
+ Map.entry("Console", OrtbDeviceType.SET_TOP_BOX),
+ Map.entry("Desktop", OrtbDeviceType.PERSONAL_COMPUTER),
+ Map.entry("EReader", OrtbDeviceType.PERSONAL_COMPUTER),
+ Map.entry("IoT", OrtbDeviceType.CONNECTED_DEVICE),
+ Map.entry("Kiosk", OrtbDeviceType.OOH_DEVICE),
+ Map.entry("MediaHub", OrtbDeviceType.SET_TOP_BOX),
+ Map.entry("Mobile", OrtbDeviceType.MOBILE_TABLET),
+ Map.entry("Router", OrtbDeviceType.CONNECTED_DEVICE),
+ Map.entry("SmallScreen", OrtbDeviceType.CONNECTED_DEVICE),
+ Map.entry("SmartPhone", OrtbDeviceType.MOBILE_TABLET),
+ Map.entry("SmartSpeaker", OrtbDeviceType.CONNECTED_DEVICE),
+ Map.entry("SmartWatch", OrtbDeviceType.CONNECTED_DEVICE),
+ Map.entry("Tablet", OrtbDeviceType.TABLET),
+ Map.entry("Tv", OrtbDeviceType.CONNECTED_TV),
+ Map.entry("Vehicle Display", OrtbDeviceType.PERSONAL_COMPUTER));
+
+ public static OrtbDeviceType resolveFrom(String deviceType) {
+ return Optional.ofNullable(DEVICE_FIELD_MAPPING.get(deviceType)).orElse(UNKNOWN);
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/PipelineBuilder.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/PipelineBuilder.java
new file mode 100644
index 00000000000..2b10e932f5f
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/PipelineBuilder.java
@@ -0,0 +1,203 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core;
+
+import fiftyone.devicedetection.DeviceDetectionOnPremisePipelineBuilder;
+import fiftyone.devicedetection.DeviceDetectionPipelineBuilder;
+import fiftyone.pipeline.core.flowelements.Pipeline;
+import fiftyone.pipeline.engines.Constants;
+import fiftyone.pipeline.engines.services.DataUpdateServiceDefault;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config.DataFile;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config.DataFileUpdate;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config.ModuleConfig;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config.PerformanceConfig;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class PipelineBuilder {
+ private static final Collection PROPERTIES_USED = List.of(
+ "devicetype",
+ "hardwarevendor",
+ "hardwaremodel",
+ "hardwarename",
+ "platformname",
+ "platformversion",
+ "screenpixelsheight",
+ "screenpixelswidth",
+ "screeninchesheight",
+ "pixelratio",
+
+ "BrowserName",
+ "BrowserVersion",
+ "IsCrawler",
+
+ "BrowserVendor",
+ "PlatformVendor",
+ "Javascript",
+ "GeoLocation",
+ "HardwareModelVariants");
+
+ private final ModuleConfig moduleConfig;
+
+ public PipelineBuilder(ModuleConfig moduleConfig) {
+ this.moduleConfig = moduleConfig;
+ }
+
+ public Pipeline build(DeviceDetectionPipelineBuilder premadeBuilder) throws Exception {
+ final DataFile dataFile = moduleConfig.getDataFile();
+
+ final Boolean shouldMakeDataCopy = dataFile.getMakeTempCopy();
+ final DeviceDetectionOnPremisePipelineBuilder builder = premadeBuilder.useOnPremise(
+ dataFile.getPath(),
+ BooleanUtils.isTrue(shouldMakeDataCopy));
+
+ applyUpdateOptions(builder, dataFile.getUpdate());
+ applyPerformanceOptions(builder, moduleConfig.getPerformance());
+ PROPERTIES_USED.forEach(builder::setProperty);
+ return builder.build();
+ }
+
+ private static void applyUpdateOptions(DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ DataFileUpdate updateConfig) {
+ if (updateConfig == null) {
+ return;
+ }
+ pipelineBuilder.setDataUpdateService(new DataUpdateServiceDefault());
+
+ resolveAutoUpdate(pipelineBuilder, updateConfig);
+ resolveUpdateOnStartup(pipelineBuilder, updateConfig);
+ resolveUpdateURL(pipelineBuilder, updateConfig);
+ resolveLicenseKey(pipelineBuilder, updateConfig);
+ resolveWatchFileSystem(pipelineBuilder, updateConfig);
+ resolveUpdatePollingInterval(pipelineBuilder, updateConfig);
+ }
+
+ private static void resolveAutoUpdate(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ DataFileUpdate updateConfig) {
+ final Boolean auto = updateConfig.getAuto();
+ if (auto != null) {
+ pipelineBuilder.setAutoUpdate(auto);
+ }
+ }
+
+ private static void resolveUpdateOnStartup(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ DataFileUpdate updateConfig) {
+ final Boolean onStartup = updateConfig.getOnStartup();
+ if (onStartup != null) {
+ pipelineBuilder.setDataUpdateOnStartup(onStartup);
+ }
+ }
+
+ private static void resolveUpdateURL(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ DataFileUpdate updateConfig) {
+ final String url = updateConfig.getUrl();
+ if (StringUtils.isNotEmpty(url)) {
+ pipelineBuilder.setDataUpdateUrl(url);
+ }
+ }
+
+ private static void resolveLicenseKey(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ DataFileUpdate updateConfig) {
+ final String licenseKey = updateConfig.getLicenseKey();
+ if (StringUtils.isNotEmpty(licenseKey)) {
+ pipelineBuilder.setDataUpdateLicenseKey(licenseKey);
+ }
+ }
+
+ private static void resolveWatchFileSystem(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ DataFileUpdate updateConfig) {
+ final Boolean watchFileSystem = updateConfig.getWatchFileSystem();
+ if (watchFileSystem != null) {
+ pipelineBuilder.setDataFileSystemWatcher(watchFileSystem);
+ }
+ }
+
+ private static void resolveUpdatePollingInterval(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ DataFileUpdate updateConfig) {
+ final Integer pollingInterval = updateConfig.getPollingInterval();
+ if (pollingInterval != null) {
+ pipelineBuilder.setUpdatePollingInterval(pollingInterval);
+ }
+ }
+
+ private static void applyPerformanceOptions(DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ PerformanceConfig performanceConfig) {
+ if (performanceConfig == null) {
+ return;
+ }
+ resolvePerformanceProfile(pipelineBuilder, performanceConfig);
+ resolveConcurrency(pipelineBuilder, performanceConfig);
+ resolveDifference(pipelineBuilder, performanceConfig);
+ resolveAllowUnmatched(pipelineBuilder, performanceConfig);
+ resolveDrift(pipelineBuilder, performanceConfig);
+ }
+
+ private static void resolvePerformanceProfile(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ PerformanceConfig performanceConfig) {
+ final String profile = performanceConfig.getProfile();
+ if (StringUtils.isEmpty(profile)) {
+ return;
+ }
+ for (Constants.PerformanceProfiles nextProfile : Constants.PerformanceProfiles.values()) {
+ if (StringUtils.equalsIgnoreCase(nextProfile.name(), profile)) {
+ pipelineBuilder.setPerformanceProfile(nextProfile);
+ return;
+ }
+ }
+ throw new IllegalArgumentException(
+ "Invalid value for performance profile ("
+ + profile
+ + ") -- should be one of: "
+ + Arrays.stream(Constants.PerformanceProfiles.values())
+ .map(Enum::name)
+ .collect(Collectors.joining(", "))
+ );
+ }
+
+ private static void resolveConcurrency(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ PerformanceConfig performanceConfig) {
+ final Integer concurrency = performanceConfig.getConcurrency();
+ if (concurrency != null) {
+ pipelineBuilder.setConcurrency(concurrency);
+ }
+ }
+
+ private static void resolveDifference(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ PerformanceConfig performanceConfig) {
+ final Integer difference = performanceConfig.getDifference();
+ if (difference != null) {
+ pipelineBuilder.setDifference(difference);
+ }
+ }
+
+ private static void resolveAllowUnmatched(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ PerformanceConfig performanceConfig) {
+ final Boolean allowUnmatched = performanceConfig.getAllowUnmatched();
+ if (allowUnmatched != null) {
+ pipelineBuilder.setAllowUnmatched(allowUnmatched);
+ }
+ }
+
+ private static void resolveDrift(
+ DeviceDetectionOnPremisePipelineBuilder pipelineBuilder,
+ PerformanceConfig performanceConfig) {
+ final Integer drift = performanceConfig.getDrift();
+ if (drift != null) {
+ pipelineBuilder.setDrift(drift);
+ }
+ }
+
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/SecureHeadersRetriever.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/SecureHeadersRetriever.java
new file mode 100644
index 00000000000..142e789adc3
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/SecureHeadersRetriever.java
@@ -0,0 +1,100 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core;
+
+import com.iab.openrtb.request.BrandVersion;
+import com.iab.openrtb.request.UserAgent;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import jakarta.annotation.Nonnull;
+
+public class SecureHeadersRetriever {
+ private SecureHeadersRetriever() {
+ }
+
+ public static Map retrieveFrom(@Nonnull UserAgent userAgent) {
+ final Map secureHeaders = new HashMap<>();
+
+ final List versions = userAgent.getBrowsers();
+ if (CollectionUtils.isNotEmpty(versions)) {
+ final String fullUA = brandListToString(versions);
+ secureHeaders.put("header.Sec-CH-UA", fullUA);
+ secureHeaders.put("header.Sec-CH-UA-Full-Version-List", fullUA);
+ }
+
+ final BrandVersion platform = userAgent.getPlatform();
+ if (platform != null) {
+ final String platformName = platform.getBrand();
+ if (StringUtils.isNotBlank(platformName)) {
+ secureHeaders.put("header.Sec-CH-UA-Platform", toHeaderSafe(platformName));
+ }
+
+ final List platformVersions = platform.getVersion();
+ if (CollectionUtils.isNotEmpty(platformVersions)) {
+ final StringBuilder stringBuilder = new StringBuilder();
+ stringBuilder.append('"');
+ appendVersionList(stringBuilder, platformVersions);
+ stringBuilder.append('"');
+ secureHeaders.put("header.Sec-CH-UA-Platform-Version", stringBuilder.toString());
+ }
+ }
+
+ final Integer isMobile = userAgent.getMobile();
+ if (isMobile != null) {
+ secureHeaders.put("header.Sec-CH-UA-Mobile", "?" + isMobile);
+ }
+
+ final String architecture = userAgent.getArchitecture();
+ if (StringUtils.isNotBlank(architecture)) {
+ secureHeaders.put("header.Sec-CH-UA-Arch", toHeaderSafe(architecture));
+ }
+
+ final String bitness = userAgent.getBitness();
+ if (StringUtils.isNotBlank(bitness)) {
+ secureHeaders.put("header.Sec-CH-UA-Bitness", toHeaderSafe(bitness));
+ }
+
+ final String model = userAgent.getModel();
+ if (StringUtils.isNotBlank(model)) {
+ secureHeaders.put("header.Sec-CH-UA-Model", toHeaderSafe(model));
+ }
+
+ return secureHeaders;
+ }
+
+ private static String toHeaderSafe(String rawValue) {
+ return '"' + rawValue.replace("\"", "\\\"") + '"';
+ }
+
+ private static String brandListToString(List versions) {
+ final StringBuilder stringBuilder = new StringBuilder();
+ for (BrandVersion nextBrandVersion : versions) {
+ final String brandName = nextBrandVersion.getBrand();
+ if (brandName == null) {
+ continue;
+ }
+ if (!stringBuilder.isEmpty()) {
+ stringBuilder.append(", ");
+ }
+ stringBuilder.append(toHeaderSafe(brandName));
+ stringBuilder.append(";v=\"");
+ appendVersionList(stringBuilder, nextBrandVersion.getVersion());
+ stringBuilder.append('"');
+ }
+ return stringBuilder.toString();
+ }
+
+ private static void appendVersionList(StringBuilder stringBuilder, List versions) {
+ if (CollectionUtils.isEmpty(versions)) {
+ return;
+ }
+
+ stringBuilder.append(versions.getFirst());
+ for (int i = 1; i < versions.size(); i++) {
+ stringBuilder.append('.');
+ stringBuilder.append(versions.get(i));
+ }
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java
new file mode 100644
index 00000000000..9df4e2a0237
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java
@@ -0,0 +1,42 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.hooks;
+
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.boundary.CollectedEvidence;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.ModuleContext;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.InvocationResultImpl;
+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 io.vertx.core.Future;
+
+public class FiftyOneDeviceDetectionEntrypointHook implements EntrypointHook {
+ private static final String CODE = "fiftyone-devicedetection-entrypoint-hook";
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+
+ @Override
+ public Future> call(
+ EntrypointPayload payload,
+ InvocationContext invocationContext) {
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.no_action)
+ .moduleContext(
+ ModuleContext
+ .builder()
+ .collectedEvidence(
+ CollectedEvidence
+ .builder()
+ .rawHeaders(payload.headers().entries())
+ .build()
+ )
+ .build())
+ .build());
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java
new file mode 100644
index 00000000000..081177e8ca1
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java
@@ -0,0 +1,153 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.hooks;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.UserAgent;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.boundary.CollectedEvidence;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config.AccountFilter;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.DeviceEnricher;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.EnrichmentResult;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.SecureHeadersRetriever;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.ModuleContext;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.InvocationResultImpl;
+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.hooks.v1.auction.RawAuctionRequestHook;
+import io.vertx.core.Future;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.util.ObjectUtil;
+
+import java.util.List;
+import java.util.Optional;
+
+public class FiftyOneDeviceDetectionRawAuctionRequestHook implements RawAuctionRequestHook {
+ private static final String CODE = "fiftyone-devicedetection-raw-auction-request-hook";
+
+ private final AccountFilter accountFilter;
+ private final DeviceEnricher deviceEnricher;
+
+ public FiftyOneDeviceDetectionRawAuctionRequestHook(AccountFilter accountFilter, DeviceEnricher deviceEnricher) {
+ this.accountFilter = accountFilter;
+ this.deviceEnricher = deviceEnricher;
+ }
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+
+ @Override
+ public Future> call(AuctionRequestPayload payload,
+ AuctionInvocationContext invocationContext) {
+ final ModuleContext oldModuleContext = (ModuleContext) invocationContext.moduleContext();
+
+ if (shouldSkipEnriching(payload, invocationContext)) {
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.no_action)
+ .moduleContext(oldModuleContext)
+ .build());
+ }
+
+ final ModuleContext moduleContext = addEvidenceToContext(
+ oldModuleContext,
+ payload.bidRequest());
+
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.update)
+ .payloadUpdate(freshPayload -> updatePayload(freshPayload, moduleContext.collectedEvidence()))
+ .moduleContext(moduleContext)
+ .build()
+ );
+ }
+
+ private boolean shouldSkipEnriching(AuctionRequestPayload payload, AuctionInvocationContext invocationContext) {
+ if (!isAccountAllowed(invocationContext)) {
+ return true;
+ }
+ final Device device = ObjectUtil.getIfNotNull(payload.bidRequest(), BidRequest::getDevice);
+ return device != null && DeviceEnricher.shouldSkipEnriching(device);
+ }
+
+ private boolean isAccountAllowed(AuctionInvocationContext invocationContext) {
+ final List allowList = ObjectUtil.getIfNotNull(accountFilter, AccountFilter::getAllowList);
+ if (CollectionUtils.isEmpty(allowList)) {
+ return true;
+ }
+ return Optional.ofNullable(invocationContext)
+ .map(AuctionInvocationContext::auctionContext)
+ .map(AuctionContext::getAccount)
+ .map(Account::getId)
+ .filter(StringUtils::isNotBlank)
+ .map(allowList::contains)
+ .orElse(false);
+ }
+
+ private ModuleContext addEvidenceToContext(ModuleContext moduleContext, BidRequest bidRequest) {
+ final CollectedEvidence.CollectedEvidenceBuilder evidenceBuilder = Optional.ofNullable(moduleContext)
+ .map(ModuleContext::collectedEvidence)
+ .map(CollectedEvidence::toBuilder)
+ .orElseGet(CollectedEvidence::builder);
+
+ collectEvidence(evidenceBuilder, bidRequest);
+
+ return Optional.ofNullable(moduleContext)
+ .map(ModuleContext::toBuilder)
+ .orElseGet(ModuleContext::builder)
+ .collectedEvidence(evidenceBuilder.build())
+ .build();
+ }
+
+ private void collectEvidence(CollectedEvidence.CollectedEvidenceBuilder evidenceBuilder, BidRequest bidRequest) {
+ final Device device = ObjectUtil.getIfNotNull(bidRequest, BidRequest::getDevice);
+ if (device == null) {
+ return;
+ }
+ final String ua = device.getUa();
+ if (ua != null) {
+ evidenceBuilder.deviceUA(ua);
+ }
+ final UserAgent sua = device.getSua();
+ if (sua != null) {
+ evidenceBuilder.secureHeaders(SecureHeadersRetriever.retrieveFrom(sua));
+ }
+ }
+
+ private AuctionRequestPayload updatePayload(AuctionRequestPayload existingPayload,
+ CollectedEvidence collectedEvidence) {
+ final BidRequest currentRequest = existingPayload.bidRequest();
+ try {
+ final BidRequest patchedRequest = enrichDevice(currentRequest, collectedEvidence);
+ return patchedRequest == null ? existingPayload : AuctionRequestPayloadImpl.of(patchedRequest);
+ } catch (Exception ignored) {
+ return existingPayload;
+ }
+ }
+
+ private BidRequest enrichDevice(BidRequest bidRequest, CollectedEvidence collectedEvidence) throws Exception {
+ if (bidRequest == null) {
+ return null;
+ }
+
+ final CollectedEvidence.CollectedEvidenceBuilder evidenceBuilder = collectedEvidence.toBuilder();
+ collectEvidence(evidenceBuilder, bidRequest);
+
+ final EnrichmentResult mergeResult = deviceEnricher.populateDeviceInfo(
+ bidRequest.getDevice(),
+ evidenceBuilder.build());
+ return Optional.ofNullable(mergeResult)
+ .map(EnrichmentResult::enrichedDevice)
+ .map(mergedDevice -> bidRequest.toBuilder().device(mergedDevice).build())
+ .orElse(null);
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java
new file mode 100644
index 00000000000..ead75085974
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java
@@ -0,0 +1,24 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model;
+
+import lombok.Builder;
+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.PayloadUpdate;
+import org.prebid.server.hooks.v1.analytics.Tags;
+
+import java.util.List;
+
+@Builder
+public record InvocationResultImpl(
+ InvocationStatus status,
+ String message,
+ InvocationAction action,
+ PayloadUpdate payloadUpdate,
+ List errors,
+ List warnings,
+ List debugMessages,
+ Object moduleContext,
+ Tags analyticsTags
+) implements InvocationResult {
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/ModuleContext.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/ModuleContext.java
new file mode 100644
index 00000000000..2ec7af61bf5
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/ModuleContext.java
@@ -0,0 +1,8 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model;
+
+import lombok.Builder;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.boundary.CollectedEvidence;
+
+@Builder(toBuilder = true)
+public record ModuleContext(CollectedEvidence collectedEvidence) {
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/main/resources/module-config/fiftyone-devicedetection.yaml b/extra/modules/fiftyone-devicedetection/src/main/resources/module-config/fiftyone-devicedetection.yaml
new file mode 100644
index 00000000000..c54ab0d86f8
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/main/resources/module-config/fiftyone-devicedetection.yaml
@@ -0,0 +1,21 @@
+hooks:
+ modules:
+ fiftyone-devicedetection:
+ account-filter:
+ allow-list: [] # list of strings
+ data-file:
+ path: ~ # string, REQUIRED, download the sample from https://github.com/51Degrees/device-detection-data/blob/main/51Degrees-LiteV4.1.hash or Enterprise from https://51degrees.com/pricing
+ make-temp-copy: ~ # boolean
+ update:
+ auto: ~ # boolean
+ on-startup: ~ # boolean
+ url: ~ # string
+ license-key: ~ # string
+ watch-file-system: ~ # boolean
+ polling-interval: ~ # int, seconds
+ performance:
+ profile: ~ # string, one of [LowMemory,MaxPerformance,HighPerformance,Balanced,BalancedTemp]
+ concurrency: ~ # int
+ difference: ~ # int
+ allow-unmatched: ~ # boolean
+ drift: ~ # int
diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/AccountFilterTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/AccountFilterTest.java
new file mode 100644
index 00000000000..1b5bee8465c
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/AccountFilterTest.java
@@ -0,0 +1,34 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AccountFilterTest {
+ private static final List TEST_ALLOW_LIST = List.of(
+ "sister",
+ "cousin"
+ );
+
+ @Test
+ public void shouldReturnAllowList() {
+ // given
+ final AccountFilter accountFilter = new AccountFilter();
+ accountFilter.setAllowList(TEST_ALLOW_LIST);
+
+ // when and then
+ assertThat(accountFilter.getAllowList()).isEqualTo(TEST_ALLOW_LIST);
+ }
+
+ @Test
+ public void shouldHaveDescription() {
+ // given
+ final AccountFilter accountFilter = new AccountFilter();
+ accountFilter.setAllowList(TEST_ALLOW_LIST);
+
+ // when and then
+ assertThat(accountFilter.toString()).isNotBlank();
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileTest.java
new file mode 100644
index 00000000000..87995a4df34
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileTest.java
@@ -0,0 +1,57 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DataFileTest {
+ @Test
+ public void shouldReturnPath() {
+ // given
+ final String path = "/path/to/file.txt";
+
+ // when
+ final DataFile dataFile = new DataFile();
+ dataFile.setPath(path);
+
+ // then
+ assertThat(dataFile.getPath()).isEqualTo(path);
+ }
+
+ @Test
+ public void shouldReturnMakeTempCopy() {
+ // given
+ final boolean makeCopy = true;
+
+ // when
+ final DataFile dataFile = new DataFile();
+ dataFile.setMakeTempCopy(makeCopy);
+
+ // then
+ assertThat(dataFile.getMakeTempCopy()).isEqualTo(makeCopy);
+ }
+
+ @Test
+ public void shouldReturnUpdate() {
+ // given
+ final DataFileUpdate dataFileUpdate = new DataFileUpdate();
+ dataFileUpdate.setUrl("www.void");
+
+ // when
+ final DataFile dataFile = new DataFile();
+ dataFile.setUpdate(dataFileUpdate);
+
+ // then
+ assertThat(dataFile.getUpdate()).isEqualTo(dataFileUpdate);
+ }
+
+ @Test
+ public void shouldHaveDescription() {
+ // given
+ final DataFile dataFile = new DataFile();
+ dataFile.setPath("/etc/null");
+
+ // when and then
+ assertThat(dataFile.toString()).isNotBlank();
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileUpdateTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileUpdateTest.java
new file mode 100644
index 00000000000..211ce5be364
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/DataFileUpdateTest.java
@@ -0,0 +1,95 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DataFileUpdateTest {
+ @Test
+ public void shouldReturnAuto() {
+ // given
+ final boolean value = true;
+
+ // when
+ final DataFileUpdate dataFileUpdate = new DataFileUpdate();
+ dataFileUpdate.setAuto(value);
+
+ // then
+ assertThat(dataFileUpdate.getAuto()).isEqualTo(value);
+ }
+
+ @Test
+ public void shouldReturnOnStartup() {
+ // given
+ final boolean value = true;
+
+ // when
+ final DataFileUpdate dataFileUpdate = new DataFileUpdate();
+ dataFileUpdate.setOnStartup(value);
+
+ // then
+ assertThat(dataFileUpdate.getOnStartup()).isEqualTo(value);
+ }
+
+ @Test
+ public void shouldReturnUrl() {
+ // given
+ final String value = "/path/to/file.txt";
+
+ // when
+ final DataFileUpdate dataFileUpdate = new DataFileUpdate();
+ dataFileUpdate.setUrl(value);
+
+ // then
+ assertThat(dataFileUpdate.getUrl()).isEqualTo(value);
+ }
+
+ @Test
+ public void shouldReturnLicenseKey() {
+ // given
+ final String value = "/path/to/file.txt";
+
+ // when
+ final DataFileUpdate dataFileUpdate = new DataFileUpdate();
+ dataFileUpdate.setLicenseKey(value);
+
+ // then
+ assertThat(dataFileUpdate.getLicenseKey()).isEqualTo(value);
+ }
+
+ @Test
+ public void shouldReturnWatchFileSystem() {
+ // given
+ final boolean value = true;
+
+ // when
+ final DataFileUpdate dataFileUpdate = new DataFileUpdate();
+ dataFileUpdate.setWatchFileSystem(value);
+
+ // then
+ assertThat(dataFileUpdate.getWatchFileSystem()).isEqualTo(value);
+ }
+
+ @Test
+ public void shouldReturnPollingInterval() {
+ // given
+ final int value = 42;
+
+ // when
+ final DataFileUpdate dataFileUpdate = new DataFileUpdate();
+ dataFileUpdate.setPollingInterval(value);
+
+ // then
+ assertThat(dataFileUpdate.getPollingInterval()).isEqualTo(value);
+ }
+
+ @Test
+ public void shouldHaveDescription() {
+ // given
+ final DataFileUpdate dataFileUpdate = new DataFileUpdate();
+ dataFileUpdate.setPollingInterval(29);
+
+ // when and then
+ assertThat(dataFileUpdate.toString()).isNotBlank();
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/ModuleConfigTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/ModuleConfigTest.java
new file mode 100644
index 00000000000..bba6dcaab05
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/ModuleConfigTest.java
@@ -0,0 +1,65 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ModuleConfigTest {
+ @Test
+ public void shouldReturnAccountFilter() {
+ // given
+ final AccountFilter accountFilter = new AccountFilter();
+ accountFilter.setAllowList(Collections.singletonList("raccoon"));
+
+ // when
+ final ModuleConfig moduleConfig = new ModuleConfig();
+ moduleConfig.setAccountFilter(accountFilter);
+
+ // then
+ assertThat(moduleConfig.getAccountFilter()).isEqualTo(accountFilter);
+ }
+
+ @Test
+ public void shouldReturnDataFile() {
+ // given
+ final DataFile dataFile = new DataFile();
+ dataFile.setPath("B:\\archive");
+
+ // when
+ final ModuleConfig moduleConfig = new ModuleConfig();
+ moduleConfig.setDataFile(dataFile);
+
+ // then
+ assertThat(moduleConfig.getDataFile()).isEqualTo(dataFile);
+ }
+
+ @Test
+ public void shouldReturnPerformanceConfig() {
+ // given
+ final PerformanceConfig performanceConfig = new PerformanceConfig();
+ performanceConfig.setProfile("SilentHunter");
+
+ // when
+ final ModuleConfig moduleConfig = new ModuleConfig();
+ moduleConfig.setPerformance(performanceConfig);
+
+ // then
+ assertThat(moduleConfig.getPerformance()).isEqualTo(performanceConfig);
+ }
+
+ @Test
+ public void shouldHaveDescription() {
+ // given
+ final DataFile dataFile = new DataFile();
+ dataFile.setPath("Z:\\virtual-drive");
+
+ // when
+ final ModuleConfig moduleConfig = new ModuleConfig();
+ moduleConfig.setDataFile(dataFile);
+
+ // when and then
+ assertThat(moduleConfig.toString()).isNotBlank();
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/PerformanceConfigTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/PerformanceConfigTest.java
new file mode 100644
index 00000000000..818e702e632
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/model/config/PerformanceConfigTest.java
@@ -0,0 +1,82 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.model.config;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PerformanceConfigTest {
+ @Test
+ public void shouldReturnProfile() {
+ // given
+ final String profile = "TurtleSlow";
+
+ // when
+ final PerformanceConfig performanceConfig = new PerformanceConfig();
+ performanceConfig.setProfile(profile);
+
+ // then
+ assertThat(performanceConfig.getProfile()).isEqualTo(profile);
+ }
+
+ @Test
+ public void shouldReturnConcurrency() {
+ // given
+ final int concurrency = 5438;
+
+ // when
+ final PerformanceConfig performanceConfig = new PerformanceConfig();
+ performanceConfig.setConcurrency(concurrency);
+
+ // then
+ assertThat(performanceConfig.getConcurrency()).isEqualTo(concurrency);
+ }
+
+ @Test
+ public void shouldReturnDifference() {
+ // given
+ final int difference = 5438;
+
+ // when
+ final PerformanceConfig performanceConfig = new PerformanceConfig();
+ performanceConfig.setDifference(difference);
+
+ // then
+ assertThat(performanceConfig.getDifference()).isEqualTo(difference);
+ }
+
+ @Test
+ public void shouldReturnAllowUnmatched() {
+ // given
+ final boolean allowUnmatched = true;
+
+ // when
+ final PerformanceConfig performanceConfig = new PerformanceConfig();
+ performanceConfig.setAllowUnmatched(allowUnmatched);
+
+ // then
+ assertThat(performanceConfig.getAllowUnmatched()).isEqualTo(allowUnmatched);
+ }
+
+ @Test
+ public void shouldReturnDrift() {
+ // given
+ final int drift = 8624;
+
+ // when
+ final PerformanceConfig performanceConfig = new PerformanceConfig();
+ performanceConfig.setDrift(drift);
+
+ // then
+ assertThat(performanceConfig.getDrift()).isEqualTo(drift);
+ }
+
+ @Test
+ public void shouldHaveDescription() {
+ // given and when
+ final PerformanceConfig performanceConfig = new PerformanceConfig();
+ performanceConfig.setProfile("LightningFast");
+
+ // when and then
+ assertThat(performanceConfig.toString()).isNotBlank();
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/FiftyOneDeviceDetectionModuleTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/FiftyOneDeviceDetectionModuleTest.java
new file mode 100644
index 00000000000..95bcc9de01d
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/FiftyOneDeviceDetectionModuleTest.java
@@ -0,0 +1,32 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1;
+
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationContext;
+import org.prebid.server.hooks.v1.Module;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class FiftyOneDeviceDetectionModuleTest {
+ @Test
+ public void shouldReturnNonBlankCode() {
+ // given
+ final Module module = new FiftyOneDeviceDetectionModule(null);
+
+ // when and then
+ assertThat(module.code()).isNotBlank();
+ }
+
+ @Test
+ public void shouldReturnSavedHooks() {
+ // given
+ final Collection> hooks = Collections.emptyList();
+ final Module module = new FiftyOneDeviceDetectionModule(hooks);
+
+ // when and then
+ assertThat(module.hooks()).isEqualTo(hooks);
+ }
+}
diff --git a/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/DeviceEnricherTest.java b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/DeviceEnricherTest.java
new file mode 100644
index 00000000000..3caca5fcc4a
--- /dev/null
+++ b/extra/modules/fiftyone-devicedetection/src/test/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/core/DeviceEnricherTest.java
@@ -0,0 +1,643 @@
+package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core;
+
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.iab.openrtb.request.Device;
+import fiftyone.devicedetection.shared.DeviceData;
+import fiftyone.pipeline.core.data.FlowData;
+import fiftyone.pipeline.core.flowelements.Pipeline;
+import fiftyone.pipeline.engines.data.AspectPropertyValue;
+import fiftyone.pipeline.engines.exceptions.NoValueException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.boundary.CollectedEvidence;
+import org.prebid.server.proto.openrtb.ext.request.ExtDevice;
+
+import java.math.BigDecimal;
+import java.util.AbstractMap;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class DeviceEnricherTest {
+
+ @Mock(strictness = LENIENT)
+ private Pipeline pipeline;
+
+ @Mock(strictness = LENIENT)
+ private FlowData flowData;
+
+ @Mock(strictness = LENIENT)
+ private DeviceData deviceData;
+
+ private DeviceEnricher target;
+
+ @BeforeEach
+ public void setUp() {
+ when(pipeline.createFlowData()).thenReturn(flowData);
+ when(flowData.get(DeviceData.class)).thenReturn(deviceData);
+ target = new DeviceEnricher(pipeline);
+ }
+
+ @Test
+ public void shouldSkipEnrichingShouldReturnFalseWhenExtIsNull() {
+ // given
+ final Device device = Device.builder().build();
+
+ // when and then
+ assertThat(DeviceEnricher.shouldSkipEnriching(device)).isFalse();
+ }
+
+ @Test
+ public void shouldSkipEnrichingShouldReturnFalseWhenExtIsEmpty() {
+ // given
+ final ExtDevice ext = ExtDevice.empty();
+ final Device device = Device.builder().ext(ext).build();
+
+ // when and then
+ assertThat(DeviceEnricher.shouldSkipEnriching(device)).isFalse();
+ }
+
+ @Test
+ public void shouldSkipEnrichingShouldReturnTrueWhenExtContainsProfileID() {
+ // given
+ final ExtDevice ext = ExtDevice.empty();
+ ext.addProperty("fiftyonedegrees_deviceId", new TextNode("0-0-0-0"));
+ final Device device = Device.builder().ext(ext).build();
+
+ // when and then
+ assertThat(DeviceEnricher.shouldSkipEnriching(device)).isTrue();
+ }
+
+ @Test
+ public void populateDeviceInfoShouldReportErrorWhenPipelineThrowsException() {
+ // given
+ final Exception e = new RuntimeException();
+ when(pipeline.createFlowData()).thenThrow(e);
+
+ // when and then
+ assertThatThrownBy(() -> target.populateDeviceInfo(null, null)).isEqualTo(e);
+ }
+
+ @Test
+ public void populateDeviceInfoShouldReportErrorWhenProcessThrowsException() {
+ // given
+ final Exception e = new RuntimeException();
+ doThrow(e).when(flowData).process();
+ final CollectedEvidence collectedEvidence = CollectedEvidence.builder().build();
+
+ // when and then
+ assertThatThrownBy(() -> target.populateDeviceInfo(null, collectedEvidence)).isEqualTo(e);
+ }
+
+ @Test
+ public void populateDeviceInfoShouldReturnNullWhenDeviceDataIsNull() throws Exception {
+ // given
+ when(flowData.get(DeviceData.class)).thenReturn(null);
+ final CollectedEvidence collectedEvidence = CollectedEvidence.builder().build();
+
+ // when
+ final EnrichmentResult result = target.populateDeviceInfo(
+ null,
+ collectedEvidence);
+
+ // then
+ assertThat(result).isNull();
+ verify(flowData, times(1)).get(DeviceData.class);
+ }
+
+ @Test
+ public void populateDeviceInfoShouldPassToFlowDataHeadersMadeFromSuaWhenPresent() throws Exception {
+ // given
+ final Map secureHeaders = Collections.singletonMap("ua", "fake-ua");
+ final CollectedEvidence collectedEvidence = CollectedEvidence.builder()
+ .secureHeaders(secureHeaders)
+ .rawHeaders(Collections.singletonMap("ua", "zumba").entrySet())
+ .build();
+
+ // when
+ target.populateDeviceInfo(null, collectedEvidence);
+
+ // then
+ final ArgumentCaptor